[The Rust Programming Language] 4. Understanding Ownership
4. Understanding Ownership
Ownership is Rust’s most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector.
In this chapter, we’ll talk about ownership as well as several related features: borrowing, slices, and how Rust lays data out in memory.
4.1 What Is Ownership?
Rust’s central feature is ownership.
All programs have to manage the way they use a computer’s memory while running. Some languages have garbage collection that constantly looks for no longer used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. None of the ownership features slow down your program while it’s running.
Ownership Rules
First, let’s take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:
-
Each value in Rust has a variable that’s called its owner.
-
There can only be one owner at a time.
-
When the owner goes out of scope, the value will be dropped.
Variable Scope
The variable s refers to a string literal, where the value of the string is hardcoded into the text of our program. The variable is valid from the point at which it’s declared until the end of the current scope. Listing 4-1 has comments annotating where the variable s is valid.
1 | { // s is not valid here, it’s not yet declared |
Listing 4-1: A variable and the scope in which it is valid
String literal VS String
String literal
String literals is a string value is hardcoded into our program. String literals are convenient, but they aren’t suitable for every situation in which we may want to use text.
-
One reason is that they’re immutable.
-
Another is that not every string value can be known when we write our code. We know the contents at compile time, so the text is hardcoded directly into the final executable.
String
String is allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time.
This kind of string can be mutated:
1 | let mut s = String::from("hello"); |
The double colon (::) is an operator that allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.
We’ll discuss this syntax more in the “Method Syntax” - https://doc.rust-lang.org/book/ch05-03-method-syntax.html#method-syntax section of Chapter 5 and when we talk about namespacing with modules in “Paths for Referring to an Item in the Module Tree” - https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html in Chapter 7.
String can be mutated but string literals cannot? The difference is how these two types deal with memory.
Memory and Allocation
Rust takes a different path: the memory is automatically returned once the variable that owns it goes out of scope. Here’s a version of our scope example from Listing 4-1 using a String instead of a string literal:
1 | { |
When a variable goes out of scope, Rust calls a special function for us. This function is called drop
, and it’s where the author of String can put the code to return the memory. Rust calls drop
automatically at the closing curly bracket.
Ways Variables and Data Interact: Move
1 | let x = 5; |
We can probably guess what this is doing: “bind the value 5 to x; then make a copy of the value in x and bind it to y.” We now have two variables, x and y, and both equal 5. This is indeed what is happening, because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.
Now let’s look at the String version:
1 | let s1 = String::from("hello"); |
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.
Ways Variables and Data Interact: Clone
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.
Here’s an example of the clone method in action:
1 | let s1 = String::from("hello"); |
When you see a call to clone
, you know that some arbitrary code is being executed and that code may be expensive.
Stack-Only Data: Copy
But this code seems to contradict what we just learned: we don’t have a call to clone, but x is still valid and wasn’t moved into y.
1 | let x = 5; |
Rust has a special annotation called the Copy trait that we can place on types like integers that are stored on the stack (we’ll talk more about traits in Chapter 10). If a type implements the Copy trait, an older variable is still usable after assignment. Rust won’t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait. If the type needs something special to happen when the value goes out of scope and we add the Copy annotation to that type, we’ll get a compile-time error. To learn about how to add the Copy annotation to your type to implement the trait, see “Derivable Traits” - https://doc.rust-lang.org/book/appendix-03-derivable-traits.html in Appendix C.
So what types implement the Copy trait? You can check the documentation for the given type to be sure, but as a general rule, any group of simple scalar values can implement Copy, and nothing that requires allocation or is some form of resource can implement Copy. Here are some of the types that implement Copy:
-
All the integer types, such as u32.
-
The Boolean type, bool, with values true and false.
-
All the floating point types, such as f64.
-
The character type, char.
-
Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.
Ownership and Functions
The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.
1 | fn main() { |
Return Values and Scope
Returning values can also transfer ownership.
1 | fn main() { |
The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless the data has been moved to be owned by another variable.
It’s possible to return multiple values using a tuple, as shown in Listing 4-5.
1 | fn main() { |
4.2 References and Borrowing
Here is how you would define and use a calculate_length function that has a reference to an object as a parameter instead of taking ownership of the value:
1 | fn main() { |
The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because it does not own it, the value it points to will not be dropped when the reference goes out of scope.
Likewise, the signature of the function uses & to indicate that the type of the parameter s is a reference. Let’s add some explanatory annotations:
Note: The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator, *. We’ll see some uses of the dereference operator in Chapter 8 and discuss details of dereferencing in Chapter 15.
So what happens if we try to modify something we’re borrowing?
1 | fn main() { |
Here’s the error:
1 | cargo run |
Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.
Mutable References
We can fix the error in the code
1 | fn main() { |
First, we had to change s to be mut. Then we had to create a mutable reference with &mut
s and accept a mutable reference with some_string: &mut String
.
But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data in a particular scope. This code will fail:
1 | let mut s = String::from("hello"); |
Here’s the error:
1 | cargo run |
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.
Data races cause undefined behavior and can be difficult to diagnose and fix when you’re trying to track them down at runtime; Rust prevents this problem from happening because it won’t even compile code with data races!
As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:
1 | let mut s = String::from("hello"); |
A similar rule exists for combining mutable and immutable references. This code results in an error:
1 | let mut s = String::from("hello"); |
Here’s the error:
1 | cargo run |
Whew! We also cannot have a mutable reference while we have an immutable one. Users of an immutable reference don’t expect the values to suddenly change out from under them! However, multiple immutable references are okay because no one who is just reading the data has the ability to affect anyone else’s reading of the data.
Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used. For instance, this code will compile because the last usage of the immutable references occurs before the mutable reference is introduced:
1 | let mut s = String::from("hello"); |
Dangling References
Let’s try to create a dangling reference, which Rust will prevent with a compile-time error:
1 | fn main() { |
Here’s the error:
1 | cargo run |
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. That’s no good! Rust won’t let us do this.
The solution here is to return the String directly:
1 | fn no_dangle() -> String { |
This works without any problems. Ownership is moved out, and nothing is deallocated.
The Rules of References
Let’s recap what we’ve discussed about references:
-
At any given time, you can have either one mutable reference or any number of immutable references.
-
References must always be valid.
4.3 The Slice Type
Another data type that does not have ownership is the slice. Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.
1 | fn main() { |
String Slices
A string slice is a reference to part of a String, and it looks like this:
1 | let s = String::from("hello world"); |
This is similar to taking a reference to the whole String but with the extra [0…5] bit. Rather than a reference to the entire String, it’s a reference to a portion of the String.
With Rust’s ..
range syntax, if you want to start at the first index (zero), you can drop the value before the two periods. In other words, these are equal:
1 | let s = String::from("hello"); |
By the same token, if your slice includes the last byte of the String, you can drop the trailing number. That means these are equal:
1 | let s = String::from("hello"); |
You can also drop both values to take a slice of the entire string. So these are equal:
1 | let s = String::from("hello"); |
Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error. For the purposes of introducing string slices, we are assuming ASCII only in this section; a more thorough discussion of UTF-8 handling is in the [“Storing UTF-8 Encoded Text with Strings” - https://doc.rust-lang.org/book/ch08-02-strings.html#storing-utf-8-encoded-text-with-strings)(https://doc.rust-lang.org/book/ch08-02-strings.html#storing-utf-8-encoded-text-with-strings) section of Chapter 8.
With all this information in mind, let’s rewrite first_word to return a slice. The type that signifies “string slice” is written as &str:
1 | fn first_word(s: &String) -> &str { |
Here’s the compiler error:
1 | cargo run |
Recall from the borrowing rules that if we have an immutable reference to something, we cannot also take a mutable reference. Because clear needs to truncate the String, it needs to get a mutable reference. Rust disallows this, and compilation fails. Not only has Rust made our API easier to use, but it has also eliminated an entire class of errors at compile time!
String Literals Are Slices
Recall that we talked about string literals being stored inside the binary. Now that we know about slices, we can properly understand string literals:
1 | let s = "Hello, world!"; |
The type of s here is &str
: it’s a slice pointing to that specific point of the binary. This is also why string literals are immutable; &str
is an immutable reference.
String Slices as Parameters
If we have a string slice, we can pass that directly. If we have a String, we can pass a slice of the entire String. Defining a function to take a string slice instead of a reference to a String makes our API more general and useful without losing any functionality:
1 | fn main() { |
Other Slices
This slice has the type &[i32]
. It works the same way as string slices do, by storing a reference to the first element and a length. You’ll use this kind of slice for all sorts of other collections. We’ll discuss these collections in detail when we talk about vectors in Chapter 8.
1 | let a = [1, 2, 3, 4, 5]; |