Most programs nowadays have garbage collectors that automatically look for unused memory; for some programs, you still have to allocate and free it by yourself. But Rust uses Ownership, another approach that handles the memory that the compilers check.
Quick review of stack and heap
Stack and Heap
- Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data; that location is always at the top. Comparatively, allocating space on the heap requires more work because the allocator must first find enough space to hold the data and then perform bookkeeping to prepare for the next allocation.
- Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory. Continuing the analogy, consider a server at a restaurant taking orders from many tables. Getting all the orders at one table is most efficient before moving on to the following table. Taking an order from table A, then an order from table B, one from A again, and then one from B again would be a much slower process. A processor can do its job better if it works on data close to other data (as it is on the stack) rather than farther away (as it can be on the heap).
- Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don’t run out of space are all problems that ownership addresses. Once you understand ownership, you won’t need to think about the stack and the heap very often, but knowing that the primary purpose of ownership is to manage heap data can help explain why it works the way it does.
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.
Move
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
When we assign s1 to s2, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to.
To ensure memory safety, after the line let s2 = s1;, Rust considers s1 as no longer valid. Therefore, Rust doesn’t need to free anything when s1 goes out of scope. Check out what happens when you try to use s1 after s2 is created; it won’t work:
You’ll get an error because Rust prevents you from using the invalidated reference.
The concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of being called a shallow copy, it’s known as a move.
Clone
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
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. We’ll discuss method syntax in Chapter 5, but because methods are a common feature in many programming languages, you’ve probably seen them before.
Stack-Only Data: Copy