Varible-Data Interaction: Move
In Rust, multiple variables can interact with the same data in different ways. Consider Example 4-2, which uses integers:
let x = 5;
let y = x;
Example 4-2: Assigning the integer value of variable x to y.
We can roughly guess what this does: "Bind 5 to x; then make a copy of the value x and bind it to y." Now we have two variables, x and y, both equal to 5. Indeed, this is what happens because integers are simple values with a known fixed size, so both 5s are placed on the stack.
Now look at the String version:
let s1 = String::from("hello");
let s2 = s1;
This looks very similar to the previous code, so we might assume it works the same way: that is, the second line makes a copy of s1 and binds it to s2. However, this is not exactly the case.
Take a look at Figure 4-1 to see what happens under the hood with String. A String is made up of three parts, shown on the left: a pointer to the memory holding the string content, a length, and a capacity. This group of data is stored on the stack. The right side shows the memory on the heap holding the content.
Figure 4-1: Memory representation of a String binding the value "hello" to s1.
Length indicates how many bytes of memory the String content is currently using. Capacity is the total bytes of memory the String has obtained from the allocator. The difference between length and capacity is important but not relevant in this context, so capacity can be ignored for now.
When we assign s1 to s2, the String's data is copied, meaning we copy its pointer, length, and capacity from the stack. We do not copy the heap data the pointer points to. In other words, the memory representation is as shown in Figure 4-2.
Figure 4-2: Memory representation of variable s2, which has a copy of s1's pointer, length, and capacity.
This representation is not like Figure 4-3, which shows what memory would look like if Rust also copied the heap data. If Rust did that, the operation s2 = s1 could have a significant performance impact on runtime when heap data is large.
Figure 4-3: Another possible memory representation for s2 = s1 if Rust also copied heap data.
Earlier we mentioned that when a variable goes out of scope, Rust automatically calls the drop function and cleans up the variable's heap memory. However, Figure 4-2 shows two data pointers pointing to the same location. This poses a problem: when s2 and s1 go out of scope, they will both try to free the same memory. This is a bug called double free, which is one of the memory safety bugs mentioned earlier. Freeing the same memory twice can lead to memory corruption and potential security vulnerabilities.
To ensure memory safety, after let s2 = s1;, Rust considers s1 no longer valid, so Rust does not need to clean up anything when s1 goes out of scope. Look at what happens when we try to use s1 after s2 is creeated; this code won't run:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
You'll get an error like this because Rust forbids using an invalid reference:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error
If you've heard the terms shallow copy and deep copy in other languages, copying the pointer, length, and capacity without copying the data might sound like a shallow copy. However, because Rust also invalidates the first variable, this operation is called a move rather than a shallow copy. The example above can be read as s1 being moved into s2. Figure 4-4 shows what actually happens.
Figure 4-4: Memory representation after s1 is invalidated.
This solves our problem! Only s2 is valid, and when it goes out of scope, it frees its own memory. Done.
Additionally, there's an implicit design choice: Rust never automatically creates "deep copies" of data. Therefore, any automatic copying can be considered to have a small performance impact at runtime.
Variable-Data Interaction: Clone
If we do want to deeply copy the heap data of a String, not just the stack data, we can use a common function called clone. Chapter 5 will discuss method syntax, but since methods are a common feature in many languages, you may have seen them before.
Here's an example of using the clone method:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
This code works fine and clearly produces the behavior shown in Figure 4-3, where the heap data is copied.
When you see a call to clone, you know that specific code is executed and that code might be quite resource-intensive. It's easy to notice that something out of the ordinary is happening.
Stack-Only Data: Copy
There's another subtle point. This code uses integers and works, as in Example 4-2:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
But this seems to contradict what we just learned: no clone call, yet x is still valid and hasn't been moved into y.
The reason is that types like integers, whose size is known at compile time, are stored entirely on the stack, so copying their actual values is fast. This means there's no reason to invalidate x after creating variable y. In other words, there's no difference between shallow and deep copy here, so calling clone wouldn't differ from the usual shallow copy, and we can ignore it.
Rust has a special annotation called the Copy trait, which can be used on types stored on the stack, like integers (traits are explained in detail in Chapter 10). If a type implements the Copy trait, an old variable is still usable after being assigned to another variable.
Rust does not allow a type to implement the Copy trait if it or any of its parts implement the Drop trait. If we use the Copy annotation on a type that needs special handling when its value goes out of scope, we'll get a compile-time error. To learn how to add the Copy annotation to your types to implement this trait, read "Derivable Traits" in Appendix C.
So which types implement the Copy trait? You can check the documentation for a given type, but as a general rule, any group of simple scalar values can implemant Copy, and any type that doesn't require allocation or some form of resource can implement Copy. Here are some Copy types:
- All integer types, like
u32. - The Boolean type,
bool, with valuestrueandfalse. - All floating-point types, like
f64. - The character type,
char. - Tuples, but only if they contain types that also implement
Copy. For example,(i32, i32)implementsCopy, but(i32, String)does not.
References Rules
Let's summarize the discussion on references:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
Slice Type
Slices allow you to reference a contiguous sequence of elements in a collection rather than the entire collection. A slice is a kind of reference, so it does not have ownership.
Structs
Structs let you create custom types that are meaningful in your domain. With structs, you can associate related pieces of data and name them, making your code clearer. In impl blocks, you can define functions associated with your type, and methods are associated functions that specify the behavior of your struct instances.
But structs are not the only way to create custom types: let's turn to Rust's enum feature to add another tool to your toolbox.