Contents

Ch.4 — Understanding Ownership

Chapter 4

Understanding Ownership


Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector, so it’s important to understand how ownership works. This chapter covers ownership as well as several related features: borrowing, slices, and how Rust lays data out in memory.

  • 4.1 What Is Ownership?
  • 4.2 References and Borrowing
  • 4.3 The Slice Type

The Rust Programming Language · Ch.4 Understanding Ownership

1

Chapter 4 · 4.1 What Is Ownership?

Ownership: a third approach to managing memory

Definition 4.1 (Ownership). A set of rules that govern how a Rust program manages memory. The compiler checks the rules; if any of them is violated, the program won’t compile.

All programs have to manage the way they use a computer’s memory while running. There are three approaches:

  • Some languages have garbage collection that regularly 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. None of the features of ownership will slow down your program while it’s running.

Ownership is a new concept for many programmers, so it takes some time to get used to. The more experienced you become with Rust and its ownership rules, the easier you’ll find it to naturally develop code that is safe and efficient. Keep at it! Understanding ownership gives a solid foundation for the features that make Rust unique; this chapter teaches it through examples that focus on a very common data structure: strings.

The Rust Programming Language · Ch.4 Understanding Ownership

2

Chapter 4 · 4.1 What Is Ownership?

The stack and the heap: the stack

Many programming languages don’t require you to think about the stack and the heap very often. But in a systems programming language like Rust, whether a value is on the stack or the heap affects how the language behaves and why you have to make certain decisions. Parts of ownership are described in relation to the stack and the heap later in this chapter, so here is a brief explanation in preparation.

Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways.

Definition 4.2 (Stack). Memory that stores values in the order it gets them and removes them in the opposite order: last in, first out (LIFO). Adding data is called pushing onto the stack; removing data is popping off the stack.

  • Think of a stack of plates: you put new plates on top of the pile and take a plate off the top. Adding or removing plates from the middle or bottom wouldn’t work as well!
  • All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time, or a size that might change, must be stored on the heap instead.

The Rust Programming Language · Ch.4 Understanding Ownership

3

Chapter 4 · 4.1 What Is Ownership?

The stack and the heap: the heap

The heap is less organized. When you 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 returns a pointer, which is the address of that location.

Definition 4.3 (Allocating on the heap). Requesting space on the heap and receiving a pointer to it; sometimes abbreviated as just allocating. Pushing values onto the stack is not considered allocating.

  • Because the pointer to the heap is a known, fixed size, you can store the pointer on the stack — but when you want the actual data, you must follow the pointer.
  • Think of being seated at a restaurant. When you enter, you state the number of people in your group, and the host finds an empty table that fits everyone and leads you there. If someone in your group comes late, they can ask where you’ve been seated to find you.

The Rust Programming Language · Ch.4 Understanding Ownership

4

Chapter 4 · 4.1 What Is Ownership?

Why the stack is faster than the heap

Storing data

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 of the stack.

Comparatively, allocating space on the heap requires more work: the allocator must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.

Accessing data

Accessing data in the heap is generally slower than on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory.

A server at a restaurant taking orders from many tables is most efficient getting all the orders at one table before moving on; A, then B, then A, then B again would be much slower. Likewise, a processor usually works better on data that’s close to other data (as on the stack) than farther away (as it can be on the heap).

The Rust Programming Language · Ch.4 Understanding Ownership

5

Chapter 4 · 4.1 What Is Ownership?

Function calls, and what ownership is for

Function calls use the stack

When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.

The problems ownership addresses

  • keeping track of what parts of code are using what data on the heap;
  • minimizing the amount of duplicate data on the heap;
  • cleaning up unused data on the heap so that you don’t run out of space.

Once you understand ownership, you won’t need to think about the stack and the heap very often. But knowing that the main purpose of ownership is to manage heap data can help explain why it works the way it does.

The Rust Programming Language · Ch.4 Understanding Ownership

6

Chapter 4 · 4.1 What Is Ownership?

The ownership rules

First, the ownership rules. Keep them in mind as we work through the examples that illustrate them:

Ownership rules.

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Remark. Now that we’re past basic Rust syntax, the examples no longer include all the fn main() { code. If you’re following along, put the following examples inside a main function manually. The examples are more concise as a result, letting us focus on the actual details rather than boilerplate code.

The Rust Programming Language · Ch.4 Understanding Ownership

7

Chapter 4 · 4.1 What Is Ownership?

Variable scope

As a first example of ownership, we’ll look at the scope of some variables.

Definition 4.4 (Scope). The range within a program for which an item is valid.

Take the following variable:

let s = "hello";

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 shows a program with comments annotating where the variable s would be valid.

The Rust Programming Language · Ch.4 Understanding Ownership

8

Chapter 4 · 4.1 What Is Ownership?

When a variable is valid

Listing 4.1: A variable and the scope in which it is valid

fn main() {

    {                      // 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

}

In other words, there are two important points in time here:

  • When s comes into scope, it is valid.
  • It remains valid until it goes out of scope.

So far, the relationship between scopes and when variables are valid is similar to that in other programming languages. Next we build on this understanding by introducing the String type.

The Rust Programming Language · Ch.4 Understanding Ownership

9

Chapter 4 · 4.1 What Is Ownership?

Why the String type illustrates ownership

To illustrate the rules of ownership, we need a data type that is more complex than those covered in the “Data Types” section of Chapter 3. The types covered previously:

  • are of a known size;
  • can be stored on the stack and popped off the stack when their scope is over;
  • can be quickly and trivially copied to make a new, independent instance if another part of code needs to use the same value in a different scope.

But we want to look at data that is stored on the heap and explore how Rust knows when to clean up that data — and the String type is a great example.

We’ll concentrate on the parts of String that relate to ownership. These aspects also apply to other complex data types, whether they are provided by the standard library or created by you. Non-ownership aspects of String are discussed in Chapter 8.

The Rust Programming Language · Ch.4 Understanding Ownership

10

Chapter 4 · 4.1 What Is Ownership?

String literals versus the String type

String literals, where a string value is hardcoded into the program, are convenient, but they aren’t suitable for every situation in which we may want to use text:

  • they’re immutable;
  • not every string value can be known when we write our code — for example, what if we want to take user input and store it?

For these situations Rust has the String type. It manages data allocated on the heap and as such can store an amount of text that is unknown to us at compile time. You can create a String from a string literal using the from function:

let s = String::from("hello");

The double colon :: operator lets us namespace this particular from function under the String type rather than using some sort of name like string_from. This syntax is discussed more in “Methods” (Chapter 5) and in “Paths for Referring to an Item in the Module Tree” (Chapter 7).

The Rust Programming Language · Ch.4 Understanding Ownership

11

Chapter 4 · 4.1 What Is Ownership?

A String can be mutated

This kind of string can be mutated:

fn main() {

    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!`

}

So, what’s the difference here? Why can String be mutated but literals cannot? The difference is in how these two types deal with memory.

The Rust Programming Language · Ch.4 Understanding Ownership

12

Chapter 4 · 4.1 What Is Ownership?

Memory and allocation

For a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal’s immutability. Unfortunately, we can’t put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while running the program.

With the String type, to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:

  1. The memory must be requested from the memory allocator at runtime.
  2. We need a way of returning this memory to the allocator when we’re done with our String.

The first part is done by us: when we call String::from, its implementation requests the memory it needs. This is pretty much universal in programming languages.

The Rust Programming Language · Ch.4 Understanding Ownership

13

Chapter 4 · 4.1 What Is Ownership?

Returning memory: the hard part

The second part — returning memory to the allocator — is different across languages.

Definition 4.5 (Garbage collector). A garbage collector (GC) keeps track of and cleans up memory that isn’t being used anymore, so the programmer doesn’t need to think about it.

In most languages without a GC, it’s our responsibility to identify when memory is no longer being used and to call code to explicitly free it, just as we did to request it. Doing this correctly has historically been a difficult programming problem:

  • If we forget, we’ll waste memory.
  • If we do it too early, we’ll have an invalid variable.
  • If we do it twice, that’s a bug too.

We need to pair exactly one allocate with exactly one free.

The Rust Programming Language · Ch.4 Understanding Ownership

14

Chapter 4 · 4.1 What Is Ownership?

Rust’s path: free memory when the owner goes out of scope

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 the scope example from Listing 4.1 using a String instead of a string literal:

fn main() {

    {

        let s = String::from("hello"); // s is valid from this point forward

 

        // do stuff with s

    }                                  // this scope is now over, and s is no

                                       // longer valid

}

There is a natural point at which we can return the memory our String needs to the allocator: when s goes out of scope. When a variable goes out of scope, Rust calls a special function for us, drop. Rust calls drop automatically at the closing curly bracket.

The Rust Programming Language · Ch.4 Understanding Ownership

15

Chapter 4 · 4.1 What Is Ownership?

The drop function

Definition 4.6 (drop). The special function Rust calls automatically when a variable goes out of scope, at the closing curly bracket. It’s where the author of a type such as String can put the code to return the memory.

Remark. In C++, this pattern of deallocating resources at the end of an item’s lifetime is sometimes called Resource Acquisition Is Initialization (RAII). The drop function in Rust will be familiar to you if you’ve used RAII patterns.

This pattern has a profound impact on the way Rust code is written. It may seem simple right now, but the behavior of code can be unexpected in more complicated situations, when we want to have multiple variables use the data we’ve allocated on the heap. The next slides explore some of those situations.

The Rust Programming Language · Ch.4 Understanding Ownership

16

Chapter 4 · 4.1 What Is Ownership?

Variables and data interacting with move

Multiple variables can interact with the same data in different ways in Rust:

Listing 4.2: Assigning the integer value of variable x to y

fn main() {

    let x = 5;

    let y = x;

}

The String version

fn main() {

    let s1 = String::from("hello");

    let s2 = s1;

}

We can probably guess what Listing 4.2 does: “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 happens, because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.

The String version looks very similar, so we might assume it works the same way: that the second line makes a copy of the value in s1 and binds it to s2. But this isn’t quite what happens.

The Rust Programming Language · Ch.4 Understanding Ownership

17

Chapter 4 · 4.1 What Is Ownership?

What a String is made of

Figure 4.1: The representation in memory of a String holding the value "hello" bound to s1

stack

heap

s1

ptr

len

5

capacity

5

0

h

1

e

2

l

3

l

4

o

Left: the stack data for s1 — its pointer, length (5) and capacity (5). Right: the string data on the heap, byte by byte (index above, value below).

A String is made up of three parts, shown on the left: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.

  • The length is how much memory, in bytes, the contents of the String are currently using.
  • The capacity is the total amount of memory, in bytes, that the String has received from the allocator.

The difference between length and capacity matters, but not in this context, so for now it’s fine to ignore the capacity.

The Rust Programming Language · Ch.4 Understanding Ownership

18

Chapter 4 · 4.1 What Is Ownership?

Assigning s1 to s2 copies only the stack data

Figure 4.2: The representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1

stack

heap

s1

ptr

len

5

capacity

5

s2

ptr

len

5

capacity

5

0

h

1

e

2

l

3

l

4

o

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.

In other words, the data representation in memory looks like Figure 4.2: two sets of stack data, s1 and s2, both pointing to the same string data on the heap.

The Rust Programming Language · Ch.4 Understanding Ownership

19

Chapter 4 · 4.1 What Is Ownership?

What s2 = s1 does not do: copy the heap data

Figure 4.3: Another possibility for what s2 = s1 might do if Rust copied the heap data as well

stack

heap

s1

ptr

len

5

capacity

5

s2

ptr

len

5

capacity

5

0

h

1

e

2

l

3

l

4

o

0

h

1

e

2

l

3

l

4

o

The representation does not look like Figure 4.3, which is what memory would look like if Rust instead copied the heap data as well: each of s1 and s2 pointing to its own copy of the string data.

If Rust did this, the operation s2 = s1 could be very expensive in terms of runtime performance if the data on the heap were large.

The Rust Programming Language · Ch.4 Understanding Ownership

20

Chapter 4 · 4.1 What Is Ownership?

The double free problem, and why s1 becomes invalid

When a variable goes out of scope, Rust automatically calls the drop function and cleans up the heap memory for that variable. But Figure 4.2 shows both data pointers pointing to the same location. This is a problem: when s2 and s1 go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities.

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:

fn main() {

    let s1 = String::from("hello");

    let s2 = s1;

 

    println!("{s1}, world!");

}

The Rust Programming Language · Ch.4 Understanding Ownership

21

Chapter 4 · 4.1 What Is Ownership?

Error E0382: borrow of moved value

You’ll get an error like this because Rust prevents you from using the invalidated reference:

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0382]: borrow of moved value: `s1`

 --> src/main.rs:5:16

  |

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!("{s1}, world!");

  |                ^^ value borrowed here after move

  |

(continued on the next slide)

The Rust Programming Language · Ch.4 Understanding Ownership

22

Chapter 4 · 4.1 What Is Ownership?

Not a shallow copy, but a move

help: consider cloning the value if the performance cost is acceptable

  |

3 |     let s2 = s1.clone();

  |                ++++++++

 

For more information about this error, try `rustc --explain E0382`.

error: could not compile `ownership` (bin "ownership") due to 1 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 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. In this example, we would say that s1 was moved into s2.

Definition 4.7 (Move). Copying a value’s stack data (pointer, length, capacity) to a new variable while invalidating the original variable, so that only the new one owns the heap data.

The Rust Programming Language · Ch.4 Understanding Ownership

23

Chapter 4 · 4.1 What Is Ownership?

After the move, only s2 is valid

Figure 4.4: The representation in memory after s1 has been invalidated (grayed out)

stack

heap

s1

ptr

len

5

capacity

5

s2

ptr

len

5

capacity

5

0

h

1

e

2

l

3

l

4

o

So, what actually happens is shown in Figure 4.4. Table s1 is grayed out because s1 is no longer valid; only s2 can be used to access the heap data.

That solves our problem! With only s2 valid, when it goes out of scope it alone will free the memory, and we’re done.

There is also a design choice implied by this: Rust will never automatically create “deep” copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.

The Rust Programming Language · Ch.4 Understanding Ownership

24

Chapter 4 · 4.1 What Is Ownership?

Scope and assignment

The inverse is true for the relationship between scoping, ownership, and memory being freed via the drop function as well. When you assign a completely new value to an existing variable, Rust will call drop and free the original value’s memory immediately. Consider this code:

fn main() {

    let mut s = String::from("hello");

    s = String::from("ahoy");

 

    println!("{s}, world!");

}

We initially declare a variable s and bind it to a String with the value "hello". Then, we immediately create a new String with the value "ahoy" and assign it to s. At this point, nothing is referring to the original value on the heap at all. Figure 4.5 (next slide) illustrates the stack and heap data now.

The Rust Programming Language · Ch.4 Understanding Ownership

25

Chapter 4 · 4.1 What Is Ownership?

Reassignment drops the old value right away

Figure 4.5: The representation in memory after the initial value has been replaced in its entirety

stack

heap

s

ptr

len

4

capacity

4

0

h

1

e

2

l

3

l

4

o

0

a

1

h

2

o

3

y

The stack data for s now points to the second piece of string data (ahoy) on the heap; the original string data (hello) is grayed out because it cannot be accessed anymore.

The original string thus immediately goes out of scope. Rust will run the drop function on it and its memory will be freed right away. When we print the value at the end, it will be "ahoy, world!".

The Rust Programming Language · Ch.4 Understanding Ownership

26

Chapter 4 · 4.1 What Is Ownership?

Variables and data interacting with 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. Method syntax is discussed in Chapter 5, but because methods are a common feature in many programming languages, you’ve probably seen them before. Here’s the clone method in action:

fn main() {

    let s1 = String::from("hello");

    let s2 = s1.clone();

 

    println!("s1 = {s1}, s2 = {s2}");

}

This works just fine and explicitly produces the behavior shown in Figure 4.3, where the heap data does get copied.

When you see a call to clone, you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on.

The Rust Programming Language · Ch.4 Understanding Ownership

27

Chapter 4 · 4.1 What Is Ownership?

Stack-only data: a puzzle with integers

There’s another wrinkle we haven’t talked about yet. This code using integers — part of which was shown in Listing 4.2 — works and is valid:

fn main() {

    let x = 5;

    let y = x;

 

    println!("x = {x}, y = {y}");

}

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.

The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. There’s no reason to prevent x from being valid after we create the variable y. In other words, there’s no difference between deep and shallow copying here, so calling clone wouldn’t do anything different from the usual shallow copying, and we can leave it out.

The Rust Programming Language · Ch.4 Understanding Ownership

28

Chapter 4 · 4.1 What Is Ownership?

The Copy trait

Rust has a special annotation called the Copy trait that we can place on types that are stored on the stack, as integers are (traits are covered in Chapter 10).

Definition 4.8 (Copy trait). If a type implements the Copy trait, 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 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.

Remark. To learn how to add the Copy annotation to your type to implement the trait, see “Derivable Traits” in Appendix C.

The Rust Programming Language · Ch.4 Understanding Ownership

29

Chapter 4 · 4.1 What Is Ownership?

Which types implement Copy?

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.

Table 4.1: Some of the types that implement Copy

TypeExamples
All the integer typesu32
The Boolean type, booltrue and false
All the floating-point typesf64
The character typechar
Tuples, if they only contain types that also implement Copy(i32, i32) implements Copy, but (i32, String) does not

The Rust Programming Language · Ch.4 Understanding Ownership

30

Chapter 4 · 4.1 What Is Ownership?

Ownership and functions

Passing a variable to a function will move or copy, just as assignment does. Listing 4.3 annotates where variables go into and out of scope.

Listing 4.3: Functions with ownership and scope annotated — src/main.rs

fn main() {

    let s = String::from("hello");  // s comes into scope

 

    takes_ownership(s);             // s's value moves into the function...

                                    // ... and so is no longer valid here

 

    let x = 5;                      // x comes into scope

 

    makes_copy(x);                  // Because i32 implements the Copy trait,

                                    // x does NOT move into the function,

                                    // so it's okay to use x afterward.

 

} // Here, x goes out of scope, then s. However, because s's value was moved,

  // nothing special happens.

The Rust Programming Language · Ch.4 Understanding Ownership

31

Chapter 4 · 4.1 What Is Ownership?

Ownership and functions: the called functions

Listing 4.3 (continued): Functions with ownership and scope annotated — src/main.rs

fn takes_ownership(some_string: String) { // some_string comes into scope

    println!("{some_string}");

} // Here, some_string goes out of scope and `drop` is called. The backing

  // memory is freed.

 

fn makes_copy(some_integer: i32) { // some_integer comes into scope

    println!("{some_integer}");

} // Here, some_integer goes out of scope. Nothing special happens.

If we tried to use s after the call to takes_ownership, Rust would throw a compile-time error. These static checks protect us from mistakes. Try adding code to main that uses s and x to see where you can use them and where the ownership rules prevent you from doing so.

The Rust Programming Language · Ch.4 Understanding Ownership

32

Chapter 4 · 4.1 What Is Ownership?

Return values and scope

Returning values can also transfer ownership. Listing 4.4 shows a function that returns some value, with annotations similar to those in Listing 4.3.

Listing 4.4: Transferring ownership of return values — src/main.rs

fn main() {

    let s1 = gives_ownership();        // gives_ownership moves its return

                                       // value into s1

 

    let s2 = String::from("hello");    // s2 comes into scope

 

    let s3 = takes_and_gives_back(s2); // s2 is moved into

                                       // takes_and_gives_back, which also

                                       // moves its return value into s3

} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing

  // happens. s1 goes out of scope and is dropped.

The Rust Programming Language · Ch.4 Understanding Ownership

33

Chapter 4 · 4.1 What Is Ownership?

Return values and scope: gives_ownership

Listing 4.4 (continued): Transferring ownership of return values — src/main.rs

fn gives_ownership() -> String {       // gives_ownership will move its

                                       // return value into the function

                                       // that calls it

 

    let some_string = String::from("yours"); // some_string comes into scope

 

    some_string                        // some_string is returned and

                                       // moves out to the calling

                                       // function

}

gives_ownership creates some_string and returns it; ownership of the returned value moves out to the caller, where it is bound to s1.

The Rust Programming Language · Ch.4 Understanding Ownership

34

Chapter 4 · 4.1 What Is Ownership?

Return values and scope: takes_and_gives_back

Listing 4.4 (continued): Transferring ownership of return values — src/main.rs

// This function takes a String and returns a String.

fn takes_and_gives_back(a_string: String) -> String {

    // a_string comes into

    // scope

 

    a_string  // a_string is returned and moves out to the calling function

}

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 ownership of the data has been moved to another variable.

The Rust Programming Language · Ch.4 Understanding Ownership

35

Chapter 4 · 4.1 What Is Ownership?

Passing ownership back and forth is tedious

While this works, taking ownership and then returning ownership with every function is a bit tedious.

  • What if we want to let a function use a value but not take ownership?
  • It’s quite annoying that anything we pass in also needs to be passed back if we want to use it again — in addition to any data resulting from the body of the function that we might want to return as well.

Rust does let us return multiple values using a tuple, as shown in Listing 4.5 on the next slide.

The Rust Programming Language · Ch.4 Understanding Ownership

36

Chapter 4 · 4.1 What Is Ownership?

Returning ownership with a tuple

Listing 4.5: Returning ownership of parameters — src/main.rs

fn main() {

    let s1 = String::from("hello");

 

    let (s2, len) = calculate_length(s1);

 

    println!("The length of '{s2}' is {len}.");

}

 

fn calculate_length(s: String) -> (String, usize) {

    let length = s.len(); // len() returns the length of a String

 

    (s, length)

}

But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Rust has a feature for using a value without transferring ownership: references.

The Rust Programming Language · Ch.4 Understanding Ownership

37

Chapter 4 · 4.2 References and Borrowing

References instead of moves

The issue with the tuple code in Listing 4.5 is that we have to return the String to the calling function so that we can still use the String after the call to calculate_length, because the String was moved into calculate_length. Instead, we can provide a reference to the String value.

Definition 4.9 (Reference). An address we can follow to access data stored at that address, where the data is owned by some other variable. A reference is like a pointer, but unlike a pointer it is guaranteed to point to a valid value of a particular type for the life of that reference.

On the next slide, calculate_length has a reference to an object as a parameter instead of taking ownership of the value.

The Rust Programming Language · Ch.4 Understanding Ownership

38

Chapter 4 · 4.2 References and Borrowing

calculate_length with a reference parameter

src/main.rs

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()

}

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.

  • All the tuple code in the variable declaration and the function return value is gone.
  • We pass &s1 into calculate_length and, in its definition, we take &String rather than String.

These ampersands represent references, and they allow you to refer to some value without taking ownership of it. Figure 4.6 depicts this concept.

The Rust Programming Language · Ch.4 Understanding Ownership

39

Chapter 4 · 4.2 References and Borrowing

A reference points at the owner

Figure 4.6: A diagram of &String s pointing at String s1

stack

heap

s

ptr

s1

ptr

len

5

capacity

5

0

h

1

e

2

l

3

l

4

o

The table for s contains only a pointer to the table for s1. The table for s1 contains the stack data for s1 and points to the string data on the heap.

Remark. The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator, *. Some uses of the dereference operator appear in Chapter 8, and details of dereferencing are discussed in Chapter 15.

The Rust Programming Language · Ch.4 Understanding Ownership

40

Chapter 4 · 4.2 References and Borrowing

A closer look at the function call

Look again at the call in main from the previous program:

let s1 = String::from("hello");

 

let len = calculate_length(&s1);

The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because the reference does not own it, the value it points to will not be dropped when the reference stops being used.

Likewise, the signature of the function uses & to indicate that the type of the parameter s is a reference:

fn calculate_length(s: &String) -> usize {

The Rust Programming Language · Ch.4 Understanding Ownership

41

Chapter 4 · 4.2 References and Borrowing

Annotated: s goes out of scope, the String is not dropped

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 is a reference to a String

    s.len()

} // Here, s goes out of scope. But because s does not have ownership of what

  // it refers to, the String is not dropped.

The scope in which the variable s is valid is the same as any function parameter’s scope, but the value pointed to by the reference is not dropped when s stops being used, because s doesn’t have ownership. When functions have references as parameters instead of the actual values, we won’t need to return the values in order to give back ownership, because we never had ownership.

The Rust Programming Language · Ch.4 Understanding Ownership

42

Chapter 4 · 4.2 References and Borrowing

Borrowing, and modifying what we borrow

Definition 4.10 (Borrowing). The action of creating a reference. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back. You don’t own it.

So, what happens if we try to modify something we’re borrowing? Try the code in Listing 4.6. Spoiler alert: it doesn’t work!

Listing 4.6: Attempting to modify a borrowed value — src/main.rs

fn main() {

    let s = String::from("hello");

 

    change(&s);

}

 

fn change(some_string: &String) {

    some_string.push_str(", world");

}

The Rust Programming Language · Ch.4 Understanding Ownership

43

Chapter 4 · 4.2 References and Borrowing

Error E0596: references are immutable by default

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference

 --> src/main.rs:8:5

  |

8 |     some_string.push_str(", world");

  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so it cannot be borrowed as mutable

  |

help: consider changing this to be a mutable reference

  |

7 | fn change(some_string: &mut String) {

  |                         +++

 

For more information about this error, try `rustc --explain E0596`.

error: could not compile `ownership` (bin "ownership") due to 1 previous error

Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.

The Rust Programming Language · Ch.4 Understanding Ownership

44

Chapter 4 · 4.2 References and Borrowing

Mutable references

We can fix the code from Listing 4.6 to allow us to modify a borrowed value with just a few small tweaks that use, instead, a mutable reference:

src/main.rs

fn main() {

    let mut s = String::from("hello");

 

    change(&mut s);

}

 

fn change(some_string: &mut String) {

    some_string.push_str(", world");

}

First, we change s to be mut. Then, we create a mutable reference with &mut s where we call the change function and update the function signature to accept a mutable reference with some_string: &mut String. This makes it very clear that the change function will mutate the value it borrows.

The Rust Programming Language · Ch.4 Understanding Ownership

45

Chapter 4 · 4.2 References and Borrowing

The big restriction: one mutable reference at a time

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. This code that attempts to create two mutable references to s will fail:

src/main.rs

fn main() {

    let mut s = String::from("hello");

 

    let r1 = &mut s;

    let r2 = &mut s;

 

    println!("{r1}, {r2}");

}

The compiler rejects it with error E0499, shown on the next slide.

The Rust Programming Language · Ch.4 Understanding Ownership

46

Chapter 4 · 4.2 References and Borrowing

Error E0499: two mutable borrows at a time

Here’s the error:

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0499]: cannot borrow `s` as mutable more than once at a time

 --> src/main.rs:5:14

  |

4 |     let r1 = &mut s;

  |              ------ first mutable borrow occurs here

5 |     let r2 = &mut s;

  |              ^^^^^^ second mutable borrow occurs here

6 |

7 |     println!("{r1}, {r2}");

  |                -- first borrow later used here

 

For more information about this error, try `rustc --explain E0499`.

error: could not compile `ownership` (bin "ownership") due to 1 previous error

The Rust Programming Language · Ch.4 Understanding Ownership

47

Chapter 4 · 4.2 References and Borrowing

Why the restriction: preventing data races

The error says that this code is invalid because we cannot borrow s as mutable more than once at a time. The first mutable borrow is in r1 and must last until it’s used in the println!, but between the creation of that mutable reference and its usage, we tried to create another mutable reference in r2 that borrows the same data as r1.

Preventing multiple mutable references to the same data at the same time allows mutation, but in a very controlled fashion. New Rustaceans struggle with it, because most languages let you mutate whenever you’d like. The benefit is that Rust can prevent data races at compile time.

Definition 4.11 (Data race). Similar to a race condition; it 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 by refusing to compile code with data races!

The Rust Programming Language · Ch.4 Understanding Ownership

48

Chapter 4 · 4.2 References and Borrowing

Multiple mutable references, just not simultaneous ones

As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:

fn main() {

    let mut s = String::from("hello");

 

    {

        let r1 = &mut s;

    } // r1 goes out of scope here, so we can make a new reference with no problems.

 

    let r2 = &mut s;

}

The Rust Programming Language · Ch.4 Understanding Ownership

49

Chapter 4 · 4.2 References and Borrowing

Combining mutable and immutable references

Rust enforces a similar rule for combining mutable and immutable references. This code results in an error:

fn main() {

    let mut s = String::from("hello");

 

    let r1 = &s; // no problem

    let r2 = &s; // no problem

    let r3 = &mut s; // BIG PROBLEM

 

    println!("{r1}, {r2}, and {r3}");

}

The compiler’s error, E0502, is on the next slide.

The Rust Programming Language · Ch.4 Understanding Ownership

50

Chapter 4 · 4.2 References and Borrowing

Error E0502: mutable while borrowed as immutable

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

 --> src/main.rs:6:14

  |

4 |     let r1 = &s; // no problem

  |              -- immutable borrow occurs here

5 |     let r2 = &s; // no problem

6 |     let r3 = &mut s; // BIG PROBLEM

  |              ^^^^^^ mutable borrow occurs here

7 |

8 |     println!("{r1}, {r2}, and {r3}");

  |                -- immutable borrow later used here

 

For more information about this error, try `rustc --explain E0502`.

error: could not compile `ownership` (bin "ownership") due to 1 previous error

Whew! We also cannot have a mutable reference while we have an immutable one to the same value.

The Rust Programming Language · Ch.4 Understanding Ownership

51

Chapter 4 · 4.2 References and Borrowing

A reference’s scope ends at its last use

Users of an immutable reference don’t expect the value to suddenly change out from under them! However, multiple immutable references are allowed, because no one who is just reading the data has the ability to affect anyone else’s reading of the data.

A reference’s scope starts from where it is introduced and continues through the last time that reference is used. For instance, this code compiles because the last usage of the immutable references is in the println!, before the mutable reference is introduced.

fn main() {

    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 Rust Programming Language · Ch.4 Understanding Ownership

52

Chapter 4 · 4.2 References and Borrowing

Non-overlapping scopes are allowed

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. These scopes don’t overlap, so this code is allowed: the compiler can tell that the reference is no longer being used at a point before the end of the scope.

Even though borrowing errors may be frustrating at times, remember that it’s the Rust compiler pointing out a potential bug early (at compile time rather than at runtime) and showing you exactly where the problem is. Then, you don’t have to track down why your data isn’t what you thought it was.

The Rust Programming Language · Ch.4 Understanding Ownership

53

Chapter 4 · 4.2 References and Borrowing

Dangling references

Definition 4.12 (Dangling pointer). A pointer that references a location in memory that may have been given to someone else, created by freeing some memory while preserving a pointer to that memory.

In languages with pointers, it’s easy to erroneously create a dangling pointer. In Rust, by contrast, the compiler guarantees that references will never be dangling references: if you have 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.

Let’s try to create a dangling reference to see how Rust prevents them with a compile-time error.

src/main.rs

fn main() {

    let reference_to_nothing = dangle();

}

 

fn dangle() -> &String {

    let s = String::from("hello");

 

    &s

}

The Rust Programming Language · Ch.4 Understanding Ownership

54

Chapter 4 · 4.2 References and Borrowing

Error E0106: missing lifetime specifier

Here’s the error (two long lines are wrapped here):

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0106]: missing lifetime specifier

 --> src/main.rs:5:16

  |

5 | fn dangle() -> &String {

  |                ^ expected named lifetime parameter

  |

  = help: this function's return type contains a borrowed value, but there is no value

          for it to be borrowed from

help: consider using the `'static` lifetime, but this is uncommon unless you're returning

      a borrowed value from a `const` or a `static`

  |

5 | fn dangle() -> &'static String {

  |                 +++++++

The Rust Programming Language · Ch.4 Understanding Ownership

55

Chapter 4 · 4.2 References and Borrowing

The key to the problem: nothing to borrow from

help: instead, you are more likely to want to return an owned value

  |

5 - fn dangle() -> &String {

5 + fn dangle() -> String {

  |

 

For more information about this error, try `rustc --explain E0106`.

error: could not compile `ownership` (bin "ownership") due to 1 previous error

This error message refers to a feature we haven’t covered yet: lifetimes (discussed in detail in Chapter 10). But, if you disregard the parts about lifetimes, the message does contain the key to why this code is a problem:

this function's return type contains a borrowed value, but there is no value

for it to be borrowed from

The Rust Programming Language · Ch.4 Understanding Ownership

56

Chapter 4 · 4.2 References and Borrowing

What happens at each stage of dangle

Let’s take a closer look at exactly what’s happening at each stage of our dangle code:

src/main.rs

fn main() {

    let reference_to_nothing = dangle();

}

 

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!

The Rust Programming Language · Ch.4 Understanding Ownership

57

Chapter 4 · 4.2 References and Borrowing

The fix: return the String itself

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:

fn main() {

    let string = no_dangle();

}

 

fn no_dangle() -> String {

    let s = String::from("hello");

 

    s

}

This works without any problems. Ownership is moved out, and nothing is deallocated.

The Rust Programming Language · Ch.4 Understanding Ownership

58

Chapter 4 · 4.2 References and Borrowing

The rules of references

A recap of what we’ve discussed about references:

The rules of references.

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.

Next, we’ll look at a different kind of reference: slices.

The Rust Programming Language · Ch.4 Understanding Ownership

59

Chapter 4 · 4.3 The Slice Type

Slices, and the first_word problem

Definition 4.13 (Slice). A reference to a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership.

A small programming problem: write a function that takes a string of words separated by spaces and returns the first word it finds in that string. If the function doesn’t find a space in the string, the whole string must be one word, so the entire string should be returned.

Remark. For the purposes of introducing slices, this section assumes ASCII only; a more thorough discussion of UTF-8 handling is in “Storing UTF-8 Encoded Text with Strings” in Chapter 8.

To understand the problem slices solve, let’s first write the signature of this function without using slices:

fn first_word(s: &String) -> ?

The Rust Programming Language · Ch.4 Understanding Ownership

60

Chapter 4 · 4.3 The Slice Type

A first attempt: return a byte index

The first_word function has a parameter of type &String. We don’t need ownership, so this is fine. (In idiomatic Rust, functions do not take ownership of their arguments unless they need to; the reasons will become clear as we keep going.)

But what should we return? We don’t really have a way to talk about part of a string. However, we could return the index of the end of the word, indicated by a space. Let’s try that, as shown in Listing 4.7.

Listing 4.7: The first_word function that returns a byte index value into the String parameter — src/main.rs

fn first_word(s: &String) -> usize {

    let bytes = s.as_bytes();

 

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return i;

        }

    }

 

    s.len()

}

The Rust Programming Language · Ch.4 Understanding Ownership

61

Chapter 4 · 4.3 The Slice Type

Walking the bytes: as_bytes, iter and enumerate

Because we need to go through the String element by element and check whether a value is a space, we convert our String to an array of bytes using the as_bytes method:

    let bytes = s.as_bytes();

Next, we create an iterator over the array of bytes using the iter method:

    for (i, &item) in bytes.iter().enumerate() {

Iterators are discussed in more detail in Chapter 13. For now, know that iter is a method that returns each element in a collection and that enumerate wraps the result of iter and returns each element as part of a tuple instead. The first element of the tuple returned from enumerate is the index, and the second element is a reference to the element. This is a bit more convenient than calculating the index ourselves.

The Rust Programming Language · Ch.4 Understanding Ownership

62

Chapter 4 · 4.3 The Slice Type

Destructuring the tuple and finding the space

Because the enumerate method returns a tuple, we can use patterns to destructure that tuple (patterns are discussed more in Chapter 6). In the for loop, we specify a pattern that has i for the index in the tuple and &item for the single byte in the tuple. Because we get a reference to the element from .iter().enumerate(), we use & in the pattern.

Inside the for loop, we search for the byte that represents the space by using the byte literal syntax. If we find a space, we return the position. Otherwise, we return the length of the string by using s.len():

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return i;

        }

    }

 

    s.len()

The Rust Programming Language · Ch.4 Understanding Ownership

63

Chapter 4 · 4.3 The Slice Type

The problem: the index is not tied to the String

We now have a way to find out the index of the end of the first word in the string, but there’s a problem. We’re returning a usize on its own, but it’s only a meaningful number in the context of the &String.

In other words, because it’s a separate value from the String, there’s no guarantee that it will still be valid in the future. Consider the program in Listing 4.8 that uses the first_word function from Listing 4.7.

Listing 4.8: Storing the result from calling the first_word function and then changing the String contents — src/main.rs

fn first_word(s: &String) -> usize {

    let bytes = s.as_bytes();

 

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return i;

        }

    }

 

    s.len()

}

The Rust Programming Language · Ch.4 Understanding Ownership

64

Chapter 4 · 4.3 The Slice Type

A stale index compiles without errors

Listing 4.8 (continued): Storing the result from calling the first_word function and then changing the String contents — src/main.rs

fn main() {

    let mut s = String::from("hello world");

 

    let word = first_word(&s); // word will get the value 5

 

    s.clear(); // this empties the String, making it equal to ""

 

    // word still has the value 5 here, but s no longer has any content that we

    // could meaningfully use with the value 5, so word is now totally invalid!

}

This program compiles without any errors and would also do so if we used word after calling s.clear(). Because word isn’t connected to the state of s at all, word still contains the value 5. We could use that value 5 with the variable s to try to extract the first word out, but this would be a bug because the contents of s have changed since we saved 5 in word.

The Rust Programming Language · Ch.4 Understanding Ownership

65

Chapter 4 · 4.3 The Slice Type

A second_word function would be even more brittle

Having to worry about the index in word getting out of sync with the data in s is tedious and error-prone! Managing these indices is even more brittle if we write a second_word function. Its signature would have to look like this:

fn second_word(s: &String) -> (usize, usize) {

Now we’re tracking a starting and an ending index, and we have even more values that were calculated from data in a particular state but aren’t tied to that state at all. We have three unrelated variables floating around that need to be kept in sync.

Luckily, Rust has a solution to this problem: string slices.

The Rust Programming Language · Ch.4 Understanding Ownership

66

Chapter 4 · 4.3 The Slice Type

String slices

Definition 4.14 (String slice). A reference to a contiguous sequence of the elements of a String. It looks like this:

fn main() {

    let s = String::from("hello world");

 

    let hello = &s[0..5];

    let world = &s[6..11];

}

Rather than a reference to the entire String, hello is a reference to a portion of the String, specified in the extra [0..5] bit. We create slices using a range within square brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice.

Internally, the slice data structure stores the starting position and the length of the slice, which corresponds to ending_index minus starting_index. So, in let world = &s[6..11];, world would be a slice that contains a pointer to the byte at index 6 of s with a length value of 5. Figure 4.7 shows this in a diagram.

The Rust Programming Language · Ch.4 Understanding Ownership

67

Chapter 4 · 4.3 The Slice Type

A string slice in memory

Figure 4.7: A string slice referring to part of a String

stack

heap

s

ptr

len

11

capacity

11

world

ptr

len

5

0

h

1

e

2

l

3

l

4

o

5

 

6

w

7

o

8

r

9

l

10

d

The stack data of s points to the byte at index 0 of the string data "hello world" on the heap. The slice world has a length value of 5 and points to byte 6 of the same heap data.

The Rust Programming Language · Ch.4 Understanding Ownership

68

Chapter 4 · 4.3 The Slice Type

Range shorthand: dropping the start, the end, or both

With Rust’s .. range syntax, you can leave out either bound:

To start at index 0, drop the value before the two periods. These are equal:

let s = String::from("hello");

 

let slice = &s[0..2];

let slice = &s[..2];

If your slice includes the last byte of the String, drop the trailing number. These are equal:

let s = String::from("hello");

 

let len = s.len();

 

let slice = &s[3..len];

let slice = &s[3..];

Drop both values to take a slice of the entire string. These are equal:

let s = String::from("hello");

 

let len = s.len();

 

let slice = &s[0..len];

let slice = &s[..];

Remark. 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.

The Rust Programming Language · Ch.4 Understanding Ownership

69

Chapter 4 · 4.3 The Slice Type

Rewriting first_word to return a slice

src/main.rs

fn first_word(s: &String) -> &str {

    let bytes = s.as_bytes();

 

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return &s[0..i];

        }

    }

 

    &s[..]

}

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.

We get the index for the end of the word the same way we did in Listing 4.7, by looking for the first occurrence of a space. When we find a space, we return a string slice using the start of the string and the index of the space as the starting and ending indices.

Now when we call first_word, we get back a single value that is tied to the underlying data. The value is made up of a reference to the starting point of the slice and the number of elements in the slice.

The Rust Programming Language · Ch.4 Understanding Ownership

70

Chapter 4 · 4.3 The Slice Type

Slices make the stale-index bug impossible

Returning a slice would also work for a second_word function:

fn second_word(s: &String) -> &str {

We now have a straightforward API that’s much harder to mess up because the compiler will ensure that the references into the String remain valid.

Remember the bug in the program in Listing 4.8, when we got the index to the end of the first word but then cleared the string so our index was invalid? That code was logically incorrect but didn’t show any immediate errors. The problems would show up later if we kept trying to use the first word index with an emptied string.

Slices make this bug impossible and let us know much sooner that we have a problem with our code. Using the slice version of first_word will throw a compile-time error, as the next slides show.

The Rust Programming Language · Ch.4 Understanding Ownership

71

Chapter 4 · 4.3 The Slice Type

Clearing the String while a slice is alive

The program from Listing 4.8, now using the slice version of first_word (src/main.rs):

fn first_word(s: &String) -> &str {

    let bytes = s.as_bytes();

 

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return &s[0..i];

        }

    }

 

    &s[..]

}

fn main() {

    let mut s = String::from("hello world");

 

    let word = first_word(&s);

 

    s.clear(); // error!

 

    println!("the first word is: {word}");

}

The compiler error is on the next slide.

The Rust Programming Language · Ch.4 Understanding Ownership

72

Chapter 4 · 4.3 The Slice Type

Error E0502: clear needs a mutable borrow

$ cargo run

   Compiling ownership v0.1.0 (file:///projects/ownership)

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

  --> src/main.rs:18:5

   |

16 |     let word = first_word(&s);

   |                           -- immutable borrow occurs here

17 |

18 |     s.clear(); // error!

   |     ^^^^^^^^^ mutable borrow occurs here

19 |

20 |     println!("the first word is: {word}");

   |                                   ---- immutable borrow later used here

 

For more information about this error, try `rustc --explain E0502`.

error: could not compile `ownership` (bin "ownership") due to 1 previous error

The Rust Programming Language · Ch.4 Understanding Ownership

73

Chapter 4 · 4.3 The Slice Type

Why it fails, and string literals as slices

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. The println! after the call to clear uses the reference in word, so the immutable reference must still be active at that point. Rust disallows the mutable reference in clear and the immutable reference in word from existing at the same time, 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 as slices

Recall that string literals are stored inside the binary. Now that we know about slices, we can properly understand string literals:

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.

The Rust Programming Language · Ch.4 Understanding Ownership

74

Chapter 4 · 4.3 The Slice Type

String slices as parameters

Knowing that you can take slices of literals and String values leads us to one more improvement on first_word, and that’s its signature:

fn first_word(s: &String) -> &str {

A more experienced Rustacean would write the signature shown in Listing 4.9 instead, because it allows us to use the same function on both &String values and &str values.

Listing 4.9: Improving the first_word function by using a string slice for the type of the s parameter

fn first_word(s: &str) -> &str {

    let bytes = s.as_bytes();

 

    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' {

            return &s[0..i];

        }

    }

 

    &s[..]

}

The Rust Programming Language · Ch.4 Understanding Ownership

75

Chapter 4 · 4.3 The Slice Type

Calling first_word with String values

Listing 4.9 (continued): Improving the first_word function by using a string slice for the type of the s parameter

fn main() {

    let my_string = String::from("hello world");

 

    // `first_word` works on slices of `String`s, whether partial or whole.

    let word = first_word(&my_string[0..6]);

    let word = first_word(&my_string[..]);

    // `first_word` also works on references to `String`s, which are equivalent

    // to whole slices of `String`s.

    let word = first_word(&my_string);

If we have a String, we can pass a slice of the String — partial or whole — or a reference to the String.

The Rust Programming Language · Ch.4 Understanding Ownership

76

Chapter 4 · 4.3 The Slice Type

Calling first_word with string literals

Listing 4.9 (continued)

    let my_string_literal = "hello world";

 

    // `first_word` works on slices of string literals, whether partial or

    // whole.

    let word = first_word(&my_string_literal[0..6]);

    let word = first_word(&my_string_literal[..]);

 

    // Because string literals *are* string slices already,

    // this works too, without the slice syntax!

    let word = first_word(my_string_literal);

}

If we have a string slice, we can pass that directly. This flexibility takes advantage of deref coercions, a feature covered in “Using Deref Coercions in Functions and Methods” in Chapter 15.

The Rust Programming Language · Ch.4 Understanding Ownership

77

Chapter 4 · 4.3 The Slice Type

A more general API, and other slices

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.

Other slices

String slices are specific to strings. But there’s a more general slice type too. Consider this array:

let a = [1, 2, 3, 4, 5];

Just as we might want to refer to part of a string, we might want to refer to part of an array:

let a = [1, 2, 3, 4, 5];

 

let slice = &a[1..3];

 

assert_eq!(slice, &[2, 3]);

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; they are discussed in detail with vectors in Chapter 8.

The Rust Programming Language · Ch.4 Understanding Ownership

78

Chapter 4 · Summary

Chapter summary

The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time. The Rust language gives you control over your memory usage in the same way as other systems programming languages. But having the owner of data automatically clean up that data when the owner goes out of scope means you don’t have to write and debug extra code to get this control.

  • Ownership: each value has one owner; when the owner goes out of scope, the value is dropped. Assignment and passing to functions move heap data; Copy types are copied; clone deep-copies explicitly.
  • Borrowing: either one mutable reference or any number of immutable references at a time, and references must always be valid.
  • Slices: references to a contiguous part of a collection, tied to the underlying data (&str, &[i32]).

Remark. Ownership affects how lots of other parts of Rust work, so these concepts come up throughout the rest of the book. Next, Chapter 5 looks at grouping pieces of data together in a struct.

The Rust Programming Language · Ch.4 Understanding Ownership

79