Ownership
Rust manages memory through a system of ownership with scope-based deallocation. This ensures memory safety without requiring a garbage collector. The core principles are:
- Every value has a specific variable assigned as its owner.
- There can only be one owner at any given moment.
- The value is dropped (deallocated) when the owner variable goes out of scope.
Data Interaction: Moving and Cloning
Move Semantics
When assigning a String (or any heap-allocated data) to another variable, the ownership is transferred. The original variable becomes invalid.
let original = String::from("data");
let transferred = original;
// `original` is no longer valid here and cannot be used
Cloning
If you need to retain the original variable, you must explicitly use the .clone() method to perform a deep copy of the heap data.
let original = String::from("data");
let duplicate = original.clone();
// Both `original` and `duplicate` are valid
Copy Trait
Types stored entirely on the stack, such as integers, booleans, floating-point numbers, and tuples (containing only Copy types), implement the Copy trait. These are copied automatically rather than moved.
Ownership and Functions
Passing a value to a function behaves similarly to assignment. If the type does not implement the Copy trait, ownership is moved to the function parameter. Returning values can transfer ownership back to the caller.
let mut resource = String::from("resource");
resource = take_ownership(resource); // Ownership moved to function and returned
fn take_ownership(data: String) -> String {
data // Ownership is returned to the caller
}
References and Borrowing
The & symbol denotes a reference, which allows you to access data without taking ownership. This action is referrred to as borrowing. The * symbol is used for dereferencing.
Immutable References
You can pass a reference to a function to read data without modifying it. The referenced value must remain valid for as long as the reference is active.
let message = String::from("hello world");
let length = compute_length(&message); // Borrows `message` without taking ownership
fn compute_length(s: &String) -> usize {
s.len()
} // `s` goes out of scope, but since it does not have ownership, nothing is dropped
Mutable References
To modify borrowed data, you must use a mutable reference. Both the variable and the reference must be declared with the mut keyword.
let mut editable = String::from("base");
modify(&mut editable);
fn modify(s: &mut String) {
s.push_str(" text");
}
Borrowing Rules and Data Races
Rust enforces strict borrowing rules at compile time to prevent data races. A data race occurs when:
- Two or more pointers access the same data simultaneously.
- At least one pointer is being used to write to the data.
- There is no mechanism to synchronize the access.
The specific restriction regarding references is:
You may have either one mutable reference OR any number of immutable references, but never both simultaneously. References must always be valid.
Scope Examples
The following examples illustrate how the compiler enforces these rules based on variable scope.
// Scenario A: Multiple mutable references simultaneously
let mut data = String::from("example");
let r1 = &mut data;
let r2 = &mut data;
// Error: Cannot borrow `data` as mutable more than once at a time
println!("{}, {}", r1, r2);
// Scenario B: Mixing mutable and immutable references
let mut data = String::from("example");
let r1 = &data; // No problem
let r2 = &data; // No problem
let r3 = &mut data; // Error: Cannot borrow `data` as mutable because it is also borrowed as immutable
println!("{}, {}, and {}", r1, r2, r3);
// Scenario C: Valid usage (Non-overlapping scopes)
let mut data = String::from("example");
let r1 = &data; // No problem
let r2 = &data; // No problem
println!("{} and {}", r1, r2);
// The scope of r1 and r2 ends here after their last usage
let r3 = &mut data; // No problem, as r1 and r2 are no longer used
println!("{}", r3);