Ownership
Ownership in Rust is very strict when compared to other languages. This allows it to be memory safe without the need for a garbage collector. If any of the ownership rules are broken, the code will not compile. The ownership rules for Rust are:
- 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.
Stack vs Heap
The stack is last in, first out. All items on the stack must have a fixed size, so anything with unknown size at compile time goes on the heap. The stack is faster because you don't have to search for space to put the data. When you call a function, the values getting passed to the function and the function's local variables get pushed onto the stack, and get popped off when the function ends.
The heap is less organized. You have to look for a space to place the data, and when it does you receive a pointer to that data. This pointer is then stored on the stack because it is a fixed size. Allocating to the heap takes more work, and accessing data takes more time because you have to follow the pointer. Once the pointer goes out of scope, the memory on the heap is automatically freed.
Transfering ownership
In Rust when you copy a pointer, the ownership is transfered to the new variable and the first variable is no longer valid. In order to do a deep copy of the data on the heap, you can use the common clone
method. This doesn't apply to basic types with a known size however, because they are fixed size and stored on the stack they are cheap to copy.
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // This will throw because ownership has been transfered to s2.
let s3 = String::from("hello");
let s4 = s3.clone();
println!("s3 = {}, s4 = {}", s3, s4); // Works fine
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // Works fine
Passing values into functions follow the same ownership transfering rules as assignment. Variables passed into a function are moved into the scope of the function and fall off the stack when the function ends. If a value is returned from a function, it is assigned to a variable in the scope of the function caller.
References and Borrowing
A reference is like a pointer in that it's an address you can follow to get to a piece of data, except someone else is the owner of that data. A reference is guaranteed to point to a valid point for the lifetime of the reference. If you pass a reference to a variable into a function, that variable is still valid after the function call because only the reference, or the pointer to the pointer, goes out of scope. Since it does not own the data, the data remains valid. Not that you need to include the &
when calling the function as well, acknowledging that you know this function takes a reference.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Mutable References
By default, reference are immutable. You can however declare mutable reference with &mut
.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
The one big restriction is that you can only define one mutable reference to a given value in the current scope. You can have multiple normal references to value however because they are read only by definition.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2); // Will error when used
let mut s2 = String::from("hello");
let r4 = &s2; // no problem
let r5 = &s2; // no problem
let r6 = &mut s2; // BIG PROBLEM
println!("{}, {}, and {}", r4, r5, r6); // Will eerror when used
Edge case
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
Lifetimes
Lifetime is the scope for which a given reference is valid. In the case where the lifetime is ambiguous, we must annotate the variable to let other pieces of our code know how long it will be alive for. Lifetimes are denoted with the '
character, and can be anything as long it is all lowercase characters.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Automatic lifetimes (Elision)
- Compiler automatically assigns one lifetime to each input parameter that is a reference.
- If there is exactly one lifetime parameter, that lifetime is assigned to all outputs references.
- If there are multiple input lifetime parameters and one of them is
&self
or&mut self
, the lifetime ofself
is applyed to all output references.
In functions
When using in a function definition, you say that the string slice arguments will live for a lifetime 'a
, which is equal to the lifetime of the return value (because it is essentially one of the inputs). This lifetime is equal to the shortest of the two variables' lifetimes, meaning you cannot use the return string after any of the arguments have gone out of scope.
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result); // This will throw because string2 is out of scope
Lifefimes in functions only need to be defined when the input influences the output.
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
In structs
Similar to functions, member variables in structs may have a lifetime if they are a reference. Then, the struct must live for as long as the owner of the reference.
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
In implementatons
For method implementations, you use the same syntax as generics. For method functions that take &self
as an argument, the third elision rule applies and the return value has the lifetime of the instance.
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
Static Lifetimes
This is a special lifetime that denotes that the reference can life for the entire duration of the program. String literals are an example since they are built into the binary so they are always available.
let s: &'static str = "I have a static lifetime.";