Ownership
** Ownership ** is a set of rules that govern how a Rust program manages memory.
Memory is managed through a system of ownership with a set of rules that the compiler checks
Stack and Heap
- The stack stores values in the order it gets them and removes the values in the opposite order (LIFO)
- All data stored on the stack must have a known, fixed size.
- Data with an unknown isze at compile time or a size that might change must be stored on the heap instad.
- When put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and return a pointer, which is the address of that location.
- Because the pointer to the heap is a known, fixed size, you can store the pointer on the stack.
- Pushing to the stack is faster than allocating on the heap
- When the code calls a function, the values passed into the function and the function's local variables get pushed onto the stack. When the function is over, those values get popped off the stack.
Ownership 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.
Variable Scope
A scope is the range within a program of which an item is valid.
{
// s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
- A String type manages data allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time.
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{s}"); // this will print `hello, world!`
- String can mutate but literal CANNOT
Memory and Allocation
- In the case of string literal, we know the contents at the compile time, so the text is hardcoded directly into the final binary. This is why string literals are fast and efficient.
- For String, the memory must be requested from the memory allocator at runtime, and we need a way of returning this memory to the allocator when we're done with our String
- When a variable goes out of scope, Rust calls a special function drop() for us. Rust calls drop() automatically at the closing curly bracket.
- The move concept can avoid double free memory safety bug.
let s1 = String::from("hello");
let s2 = s1; // s1 was moved to s2; s1 is no longer valid and pointing to a data on heap that needs to be "dropped"
Rust will never automatically create "deep" copies of your data
- When assign a completely new value to an existing variable, Rust will call drop() and free the original value's memory immediately.
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!"); // print out "ahoy, world!"
- If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone().
- Rust has a special annotation called Copy* trait that we can place on types that are stored on the stack
- If a type implements the Copy, variables that use it DO NOT move, but rather are trivially copied, making them still valid after assignment to another variable.
- Rust won't let us annotate a type with Copy if the type, or any part of its parts, has implemented the Drop trait.
References and 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() }
we call the action of creating a reference borrowing.
A variable by default is immutable; so is a reference
A mutable reference has one BIG restriction: if there is a mutable reference to a value, you can have NO OTHER references to that value.
Data race
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to wrtie to the data
- There's no mechanism being used to synchronize access to the data
Data races cause undefined behavior and can be difficult to diagnose and fix.
We also cannot have a mutable reference while we have an immutable one to the same value
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}");
The scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created.
Dangling references
In Rust, the compiler guarantees that a reference will never be a dangling reference. If there is a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
CONCLUSION
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
Slice type
Slice let you reference a contiguous sequence of elements in a Collection.
A slice is a kind of reference, it does not have ownership.
let s = String::from("hello");
let len = s.len();
// same
let slice = &s[0..2];
let slice = &s[..2];
// same
let slice = &s[2..len];
let slice = &s[2..];
//same
let slice = &s[0..len];
let slice = &s[..]