Skip to main content

Ownership in Rust

2024-08-258 min
RustOwnershipBorrowingMemory Management

In Rust, to guarantee memory safety and avoid errors like null pointers, double memory releases, or race conditions, there is the concept of ownership. This system works at compile time and allows Rust to avoid needing a garbage collector.

Basic Principles of Ownership

Each value has a single owner

When a value is created, it is assigned to a variable, and that variable becomes the "owner" of that value.

There can only be one owner at a time, meaning that when the owner goes out of scope (for example, when the variable leaves its scope), the associated value is automatically released by Rust. An example of this is passing a variable to a function:

rust
let text = "Hello";

// Ownership passes to the transform function
transform(text);

The owner of a value is responsible for its release

When the owner of a value goes out of scope, Rust automatically calls a destructor (the drop method), freeing the memory associated with that value.

This prevents memory leaks and ensures resources are released in a timely manner.

Ownership Transfer (Move)

Ownership can be transferred (moved) from one variable to another. When ownership is moved, the original variable is no longer valid and cannot be used.

rust
let text = String::from("hello");
let text_now = text;  // Ownership of the "hello" string moves to text_now.

// Now text is no longer valid and cannot be used.
// println!("{}", text);  // This would cause a compile error.

Borrowing

Instead of transferring ownership, Rust allows "borrowing" a value, meaning you can access a value without transferring its ownership. There are two types of borrows:

  • Immutable borrows (**&T**): Allows accessing the value without modifying it.

  • Mutable borrows (**&mut T**): Allows modifying the value, but only one mutable borrow can exist at a time.

rust
fn main() {
    let text = String::from("hello");
    let len = calculate_length(&text);  // We borrow an immutable reference of text

    println!("The length of '{}' is {}.", text, len);
}

fn calculate_length(text: &String) -> usize {  // `text` is an immutable reference
    text.len()
}

In this example:

  • **&text** is an immutable borrow of text to the calculate_length function.

  • **text** remains valid after the borrow because we only borrowed its reference, not moved its ownership.

Value Copies

Some data types in Rust implement the Copy trait, which means that instead of moving the value, a copy is made. This happens automatically for simple types like i32, f64, bool, and others. However, for more complex types like String or Vec, we must explicitly use the clone method if we want a full copy of the value.

rust
fn main() {
    let text = String::from("hello");
    let len = calculate_length(text.clone());  // We create a copy of the variable

    println!("The length of the copy of 'hello' is {}.", len);

    // Here, `text` is still valid because we created a copy, not moved the value.
    println!("The original text is still '{}'.", text);
}

fn calculate_length(text: String) -> usize {  // `text` is a copy
    text.len()
}

It's important to note that the clone method makes a full copy of the value in memory, which can be costly for large data structures.

Why is Ownership Important?

  1. Memory Safety:

  2. Avoids Common Errors:

  3. Performance:

Conclusion

Ownership is a unique and powerful feature of Rust that provides a memory safety model without compromising performance. Thanks to this concept, we can ensure that the resulting code is safe and free of memory errors, which is crucial for applications where stability and efficiency are essential, such as embedded systems or critical software. Compile-time verification guarantees that the code meets these rules without runtime performance penalties, making Rust an excellent choice for developing robust and efficient software.