Ownership is rusts most unique feature. It enables rust to make memory safety guarantees without needing a garbage collector.
rules
- Each value in rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
allocation and drop trait
Rust has a String type which will create a mutable string by holding a pointer
to a location on the heap as well as a length and capacity.
When the scope the string was declared in exits rust automatically calls a
special function drop which is implemented by String to clean up the memory
on the heap.
copy trait
For stack-only types which have not implemented drop we can instead implement
the copy trait which documents that our type can be copied on the stack
without needing to perform a deep copy.
double free
In non-GC languages freeing the same memory twice can lead to memory corruption and as a result security vulnerabilities.
let s1 = String::from("hello")
let s2 = s1;
println!("{s1}"); // compile error!
The above code will not compile and the reason why reveals the way rust prevents double free bugs. Remember that s1 does not itself contain the string's data, but rather is just a length, capacity and pointer to an address on the heap.
When we copy s1 into s2 rust considers s1 as no longer valid. Remember
each value in rust can only have one owner.
deep copy
If we did want to instead make an actual deep copy we would just use the method
.clone():
let s1 = String::from("hello")
let s2 = s1.clone();
println!("{s1}"); // works!
references and borrowing
When passing a heap allocated type to a function you are moving it to that
function. You can move it back as a return type, but this is often a bit too
much ceremony and references are used instead.
borrowing
A reference is like a pointer in that it's an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
This process, of creating a reference, is called borrowing. When you borrow something you give it back when you're finished. Passing a reference does not change ownership and as a result means the value cannot be modified!
fn main() {
let s = String::from("hello");
change(&s); // This does not compile!
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
mutable references
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
If we declare s as mut and update the function to accept a mutable reference
we can pass in a mutable value to the function. This also makes it very clear
the function with mutate the value it borrows.
only one mutable reference
Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. The following code results in a compile error:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to write to the data.
- There’s no mechanism being used to synchronize access to the data.
Multiple mutable references can exist so long as they are not inside the same scope:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
only mutable OR immutable references
Similarly to creating multiple mutable references in a single scope, you also cannot created immutable AND mutable references:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM! COMPILE ERROR!
println!("{}, {}, and {}", r1, r2, r3);
Users of an immutable reference don’t expect the value to suddenly change out from under them!
However, an immutable reference's scope ends when it is last used, as a result the following is allowed because the immutable and mutable variables don't exist in the same scope:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{r3}");
dereference
The opposite of referencing by using & is dereferencing, which is accomplished
with the dereference operator *.
dangling references
Rust prevents dangling references (dangling pointers in languages with pointers):
fn main() {
// THIS CAUSES A COMPILE ERROR!
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello"); // s is a new String
&s
// we return a reference to the String, s
// However, s goes out of scope, and is dropped. Its memory goes away.
}
Because s is created inside dangle, when the code of dangle is finished, s will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String.
The solution here would be to just return the String directly (rather than a
reference to the string).