The "Begin Rust" book

See a typo? Have a suggestion? Edit this page on Github

Get new blog posts via email

Heads up This blog post series has been updated and published as an eBook by FP Complete. I'd recommend reading that version instead of these posts. If you're interested, please check out the Rust Crash Course eBook.

We've glossed over some details of lifetimes and sequences of values so far. It's time to dive in and learn about lifetimes and slices correctly.

This post is part of a series based on teaching Rust at FP Complete. If you're reading this post outside of the blog, you can find links to all posts in the series at the top of the introduction post. You can also subscribe to the RSS feed.

Printing a person

Let's look at some fairly unsurprising code:

#[derive(Debug)]
struct Person {
    name: Option<String>,
    age: Option<u32>,
}

fn print_person(person: Person) {
    match person.name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match person.age {
        Some(age) => println!("Age is {}", age),
        None => println!("No age provided"),
    }
}

fn main() {
    print_person(Person {
        name: Some(String::from("Alice")),
        age: Some(30),
    });
}

Fairly simple, and a nice demonstration of pattern matching. However, let's throw in one extra line. Try adding this at the beginning of the print_person function:

println!("Full Person value: {:?}", person);

All good. We're printing the full contents of the Person and then pattern matching. But try adding that line to the end of the function, and you'll get a compilation error:

error[E0382]: use of partially moved value: `person`
  --> main.rs:18:41
   |
9  |         Some(name) => println!("Name is {}", name),
   |              ---- value moved here
...
18 |     println!("Full Person value: {:?}", person);
   |                                         ^^^^^^ value used here after move
   |
   = note: move occurs because the value has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

NOTE This is an error with the Rust compiler I'm using, 1.30.1. However, there are plans in place to improve this situation.

The problem is that we've consumed a part of the person value, and therefore cannot display it. We can fix that by setting it again. Let's make the person argument mutable, and then fill in the moved person.name with a default None value:

fn print_person(mut person: Person) {
    match person.name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match person.age {
        Some(age) => println!("Age is {}", age),
        None => println!("No age provided"),
    }

    person.name = None;

    println!("Full Person value: {:?}", person);
}

That compiles, but now the output is confusingly:

Name is Alice
Age is 30
Full Person value: Person { name: None, age: Some(30) }

Notice how the name in the last line in None, when ideally it should be Some(Alice). We can do better, by returning the name from the match:

person.name = match person.name {
    Some(name) => {
        println!("Name is {}", name);
        Some(name)
    },
    None => {
        println!("No name provided");
        None
    }
};

But that's decidely inelegant. Let's take a step back. Do we actually need to consume/move the person.name at all? Not really. It should work to do everything by reference. So let's go back and avoid the move entirely, by using a borrow:

match &person.name {
    Some(name) => println!("Name is {}", name),
    None => println!("No name provided"),
}

Much better! We don't need to put the borrow on person.age though, since the u32 is Copyable. Here, we're pattern matching on a reference, and therefore the name is also a reference.

However, we can be more explicit about that with the ref keyword. This keyword says that, when pattern matching, we want the pattern to be a reference, not a move of the original value. (More info in the Rust book.) We end up with:

match person.name {
    Some(ref name) => println!("Name is {}", name),
    None => println!("No name provided"),
}

In our case, this is the same basic result as &person.name.

Birthday!

Let's modify our code so that, when printing the age, we also increase the age by 1. First stab is below. Note that the code won't compile, try to predict why:

match person.age {
    Some(age) => {
        println!("Age is {}", age);
        age += 1;
    }
    None => println!("No age provided"),
}

We're trying to mutate the local age binding, but it's immutable. Well, that's easy enough to fix, just replace Some(age) with Some(mut age). That compiles, but with a warning:

warning: value assigned to `age` is never read
  --> src/main.rs:16:13
   |
16 |             age += 1;
   |             ^^^
   |
   = note: #[warn(unused_assignments)] on by default

And then the output is:

Name is Alice
Age is 30
Full Person value: Person { name: Some("Alice"), age: Some(30) }

Notice how on the last line, the age is still 30, not 31. Take a minute and try to understand what's happening here... Done? Cool.

  1. We pattern match on person.age
  2. If it's Some, we need to move the age into the local age binding
  3. But since the type is u32, it will make a copy and move the copy
  4. When we increment the age, we're incrementing a copy, which is never used.

We can try solving this by taking a mutable reference to person.age:

fn print_person(person: Person) {
    match person.name {
        Some(ref name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match &mut person.age {
        Some(age) => {
            println!("Age is {}", age);
            age += 1;
        }
        None => println!("No age provided"),
    }

    println!("Full Person value: {:?}", person);
}

The compiler complains: age is a &mut u32, but we're trying to use += on it:

error[E0368]: binary assignment operation `+=` cannot be applied to type `&mut u32`
  --> src/main.rs:16:13
   |
16 |             age += 1;
   |             ---^^^^^
   |             |
   |             cannot use `+=` on type `&mut u32`
   |
   = help: `+=` can be used on 'u32', you can dereference `age`: `*age`

The compiler taketh, and the compiler giveth as well: we just need to dereference the age reference. Close, but one more error:

error[E0596]: cannot borrow field `person.age` of immutable binding as mutable
  --> src/main.rs:13:16
   |
7  | fn print_person(person: Person) {
   |                 ------ consider changing this to `mut person`
...
13 |     match &mut person.age {
   |                ^^^^^^^^^^ cannot mutably borrow field of immutable binding

error: aborting due to previous error

Again, the compiler tells us exactly how to solve the problem: make person mutable. Go ahead and make that change, and everything should work.

Exercise 1 In the case of person.name, we came up with two solutions: borrow the person.name, or use the ref keyword. The same two styles of solutions will work for our current problem. We've just demonstrated the borrow approach. Try to solve this instead using the ref keyword.

The single iterator

Let's make a silly little iterator which produces a single value. We'll track whether or not the value has been produced by using an Option:

struct Single<T> {
    next: Option<T>,
}

Let's make a helper function to create Single values:

fn single<T>(t: T) -> Single<T> {
    Single {
        next: Some(t),
    }
}

And let's write a main that tests that this works as expected:

fn main() {
    let actual: Vec<u32> = single(42).collect();
    assert_eq!(vec![42], actual);
}

If you try to compile that, you'll get an error message:

error[E0599]: no method named `collect` found for type `Single<{integer}>` in the current scope
  --> src/main.rs:12:39
   |
1  | struct Single<T> {
   | ---------------- method `collect` not found for this
...
12 |     let actual: Vec<u32> = single(42).collect();
   |                                       ^^^^^^^
   |
   = note: the method `collect` exists but the following trait bounds were not satisfied:
           `&mut Single<{integer}> : std::iter::Iterator`
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `collect`, perhaps you need to implement it:
           candidate #1: `std::iter::Iterator`

We need to provide an Iterator implementation in order to use collect(). The Item is going to be T. And we've already got a great Option<T> available for the return value from the next function:

impl<T> Iterator for Single<T> {
    type Item = T;

    fn next(&mut self) -> Option<T> {
        self.next
    }
}

Unfortunately this doesn't work:

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:21:9
   |
21 |         self.next
   |         ^^^^ cannot move out of borrowed content

error: aborting due to previous error

Oh, right. We can't move the result value out, since our next function only mutable borrows self. Let's try some pattern matching:

match self.next {
    Some(next) => Some(next),
    None => None,
}

Except this also involves moving out of a borrow, so it fails. Let's try one more time: we'll move into a local variable, set self.next to None (so it doesn't repeat the value again), and return the local variable:

fn next(&mut self) -> Option<T> {
    let res = self.next;
    self.next = None;
    res
}

Nope, the compiler is still not happy! I guess we'll just have to give up on our grand vision of a Single iterator. We could of course just cheat:

fn next(&mut self) -> Option<T> {
    None
}

But while that compiles, it fails our test at runtime:

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `[42]`,
 right: `[]`', src/main.rs:13:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Swap

What we did above was attempt to swap the self.next with a local variable. However, the borrow checker wasn't a fan of the approach we took. However, there's a helper function in the standard library, std::mem::swap, which may be able to help us. It looks like:

pub fn swap<T>(x: &mut T, y: &mut T)

And sure enough, we can use it to solve our problem:

fn next(&mut self) -> Option<T> {
    let res = None;
    std::mem::swap(res, self.next);
    res
}

Exercise 2 The code above doesn't quite compile, though the compiler can guide you to a correct solution. Try to identify the problems above and fix them yourself. Failing that, ask the compiler to help you out.

replace and take

Did you find that whole create-a-temp-variable thing a bit verbose? Yeah, it does to the authors of the Rust standard library too. There's a helper function that bypasses that temporary variable:

fn next(&mut self) -> Option<T> {
    std::mem::replace(&mut self.next, None)
}

Much nicer! However, that still seems like more work for something that should be really easy. And fortunately, yet again, it does to the authors of the Rust standard library too. This pattern of replacing the value in an Option with None and then working with the original value is common enough that they've given it a name and a method: take.

fn next(&mut self) -> Option<T> {
    self.next.take()
}

And we're done!

Lifetimes

We've briefly mentioned lifetimes in previous lessons, but it's time to get a bit more serious about them. Let's look at a simple usage of references:

struct Person {
    name: String,
    age: u32,
}

fn get_name(person: &Person) -> String {
    person.name
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let name = get_name(&alice);
    println!("Name: {}", name);
}

This code doesn't compile. Our get_name function takes a reference to a Person, and then tries to move that person's name in its result. This isn't possible. One solution would be to clone the name:

fn get_name(person: &Person) -> String {
    person.name.clone()
}

While this works, it's relatively inefficient. We like to avoid making copies when we can. Instead, let's simply return a reference to the name:

fn get_name(person: &Person) -> &String {
    &person.name
}

Hurrah! But let's make our function a little bit more complicated. We now want a function that will take two Persons, and return the name of the older one. That sounds fairly easy to write:

struct Person {
    name: String,
    age: u32,
}

fn get_older_name(person1: &Person, person2: &Person) -> &String {
    if person1.age >= person2.age {
        &person1.name
    } else {
        &person2.name
    }
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let bob = Person {
        name: String::from("Bob"),
        age: 35,
    };
    let name = get_older_name(&alice, &bob);
    println!("Older person: {}", name);
}

Unfortunately, the compiler is quite cross with us:

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:58
  |
6 | fn get_older_name(person1: &Person, person2: &Person) -> &String {
  |                                                          ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `person1` or `person2`

That error message is remarkably clear. Our function is returning a borrowed value. That value must be borrowed from somewhere. The only two options* are person1 and person2. And it seems that Rust needs to know this for some reason.

* This is a small fib, see "static lifetime" below.

Remember how we have some rules about references? References cannot outlive the original values they come from. We need to track how long the result value is allowed to live, which must be less than or equal to the time the value it came from lives. This whole concept is lifetimes.

For reasons we'll get to in a bit (under "lifetime elision"), we can often bypass the need to explicitly talk about lifetimes. However, sometimes we do need to be explicit. To do this, we introduce some new parameters. But this time, they are lifetime parameters, which begin with a single quote and are lower case. Usually, they are just a single letter. For example:

fn get_older_name<'a, 'b>(person1: &'a Person, person2: &'b Person) -> &String

We still get an error from the compiler because our return value doesn't have a lifetime. Should we choose 'a or 'b? Or maybe we should create a new 'c and try that? Let's start off with 'a. We get the error message:

error[E0623]: lifetime mismatch
  --> src/main.rs:10:9
   |
6  | fn get_older_name<'a, 'b>(person1: &'a Person, person2: &'b Person) -> &'a String {
   |                                                         ----------     ----------
   |                                                         |
   |                                                         this parameter and the return type are declared with different lifetimes...
...
10 |         &person2.name
   |         ^^^^^^^^^^^^^ ...but data from `person2` is returned here

That makes sense: since our result may come from person2, we have no guarantee that the 'a lifetime parameter is less than or equal to the 'b lifetime parameter. Fortunately, we can explicitly state that, in the same way that we state that types implement some traits:

fn get_older_name<'a, 'b: 'a>(person1: &'a Person, person2: &'b Person) -> &'a String {

And this actually compiles! Alternatively, in this case, we can just completely bypass the second lifetime parameter, and say that person1 and person2 must have the same lifetime, which must be the same as the return value:

fn get_older_name<'a>(person1: &'a Person, person2: &'a Person) -> &'a String {

If you're like me, you may think that this would be overly limiting. For example, I initially thought that with the signature above, this code wouldn't compile:

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    foo(&alice);
}

fn foo(alice: &Person) {
    let bob = Person {
        name: String::from("Bob"),
        age: 35,
    };
    let name = get_older_name(&alice, &bob);
    println!("Older person: {}", name);
}

After all, the lifetime for alice is demonstrably bigger than the lifetime for bob. However, the semantics for lifetimes in functions signatures is that all of the values have at least the same lifetime. If they happen to live a bit longer, no harm, no foul.

Requirement for multiple lifetime parameters

So can we cook up an example where multiple lifetime parameters are absolutely necessary? Sure!

fn message_and_return(msg: &String, ret: &String) -> &String {
    println!("Printing the message: {}", msg);
    ret
}

fn main() {
    let name = String::from("Alice");
    let msg = String::from("This is the message");
    let ret = message_and_return(&msg, &name);
    println!("Return value: {}", ret);
}

This code won't compile, because we need some lifetime parameters. So let's use our trick from above, and use the same parameter:

fn message_and_return<'a>(msg: &'a String, ret: &'a String) -> &'a String {

That compiles, but let's make our calling code a bit more complicated:

fn main() {
    let name = String::from("Alice");
    let ret = foo(&name);
    println!("Return value: {}", ret);
}

fn foo(name: &String) -> &String {
    let msg = String::from("This is the message");
    message_and_return(&msg, &name)
}

Now the compiler isn't happy:

error[E0597]: `msg` does not live long enough
  --> src/main.rs:14:25
   |
14 |     message_and_return(&msg, &name)
   |                         ^^^ borrowed value does not live long enough
15 | }
   | - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 12:1...
  --> src/main.rs:12:1
   |
12 | / fn foo(name: &String) -> &String {
13 | |     let msg = String::from("This is the message");
14 | |     message_and_return(&msg, &name)
15 | | }
   | |_^

We've stated that the return value must live the same amount of time as the msg parameter. But we return the return value outside of the foo function, while the msg value will not live beyond the end of foo.

The calling code should be fine, we just need to tell Rust that it's OK if the msg parameter has a shorter lifetime than the return value.

Exercise 3 Modify the signature of message_and_return so that the code compiles and runs.

Lifetime elision

Why do we sometimes get away with skipping the lifetimes, and sometimes we need to include them? There are rules in the language called "lifetime elision." Instead of trying to cover this myself, I'll refer to the Nomicon:

https://doc.rust-lang.org/nomicon/lifetime-elision.html

Static lifetime

Above, I implied that if you return a reference, then it must have the same lifetime as one of its input parameters. This mostly makes sense, because otherwise we'd have to conjure some arbitrary lifetime out of thin air. However, it's also a lie. There's one special lifetime that survives the entire program, called 'static. And here's some fun news: you've implicitly used it since the first Hello World we wrote together!

Every single string literal is in fact a reference with the lifetime of 'static.

fn name() -> &'static str {
    "Alice"
}

fn main() {
    println!("{}", name());
}

Arrays, slices, vectors, and String

Here's another place where we've been cheeky. What's the difference between String and str? Both of these have popped up quite a bit. We'll get to those in a little bit. First, we need to talk about arrays, slices, and vectors.

Arrays

To my knowledge, the best official documentation on arrays is in the API docs themselves. Arrays are contiguous blocks of memory containing a single type of data with a fixed length. The type is represented as [T; N], where T is the type of value, and N is the length of the array. And like any sane programming language, arrays are 0-indexed in Rust.

There are two syntaxes for initiating arrays. List literal syntax (like Javascript, Python, or Haskell):

fn main() {
    let nums: [u32; 5] = [1, 2, 3, 4, 5];
    println!("{:?}", nums);
}

And a repeat expression:

fn main() {
    let nums: [u32; 5] = [42; 5];
    println!("{:?}", nums);
}

You can make arrays mutable and then, well, mutate them:

fn main() {
    let mut nums: [u32; 5] = [42; 5];
    nums[2] += 1;
    println!("{:?}", nums);
}

That's very nice, but what if you need something more dynamic? For that, we have...

Vec

A Vec is a "contiguous, growable array type." You can push and pop, check its length, and access via index in O(1) time. We also have a nifty vec! macro for constructing them:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    v.push(4);
    assert_eq!(v.pop(), Some(4));
    v.push(4);
    v.push(5);
    v.push(6);
    assert_eq!(v.pop(), Some(6));
    assert_eq!(v[2], 3);
    println!("{:?}", v); // 1, 2, 3, 4, 5
}

Slices

I'm going to write a helper function that prints all the values in a Vec:

fn main() {
    let v: Vec<u32> = vec![1, 2, 3];
    print_vals(v);
}

fn print_vals(v: Vec<u32>) {
    for i in v {
        println!("{}", i);
    }
}

Of course, since this is a pass-by-value, the following doesn't compile:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vals(v);
    v.push(4);
    print_vals(v);
}

Easy enough to fix: have print_vals take a reference to a Vec:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vals(&v);
    v.push(4);
    print_vals(&v);
}

fn print_vals(v: &Vec<u32>) {
    for i in v {
        println!("{}", i);
    }
}

Unfortunately, this doesn't generalize to, say, an array:

fn main() {
    let a: [u32; 5] = [1, 2, 3, 4, 5];
    print_vals(&a);
}

This fails since print_vals takes a &Vec<u32>, but we've provided a &[u32; 5]. But this is pretty disappointing. A dynamic vector and a fixed length array behave the same for so many things. Wouldn't it be nice if there was something that generalized both of these?

Enter slices. To quote the Rust book:

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.

To make this all work, we need to change the signature of print_vals to:

fn print_vals(v: &[u32]) {

&[u32] is a reference to a slice of u32s. A slice can be created from an array or a Vec, not to mention some other cases as well. (We'll discuss how the & borrow operator works its magic in a bit.) As a general piece of advice, if you're receiving a parameter which is a sequence of values, try to use a slice, as it will give the caller much more control about where the data comes from.

I played a bit of a word game above, switching between "reference to a slice" and "a slice." Obviously we're using a reference. Can we dereference a slice reference and get the slice itself? Let's try!

fn print_vals(vref: &[u32]) {
    let v = *vref;
    for i in v {
        println!("{}", i);
    }
}

The compiler is cross with us again:

error[E0277]: the size for values of type `[u32]` cannot be known at compilation time
 --> src/main.rs:7:9
  |
7 |     let v = *vref;
  |         ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `[u32]`
  = note: to learn more, visit <https://doc.rust-lang.org/book/second-edition/ch19-04-advanced-types.html#dynamically-sized-types-and-sized>
  = note: all local variables must have a statically known size

error[E0277]: the size for values of type `[u32]` cannot be known at compilation time
 --> src/main.rs:8:14
  |
8 |     for i in v {
  |              ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `[u32]`
  = note: to learn more, visit <https://doc.rust-lang.org/book/second-edition/ch19-04-advanced-types.html#dynamically-sized-types-and-sized>
  = note: required by `std::iter::IntoIterator::into_iter`

error[E0277]: the trait bound `[u32]: std::iter::Iterator` is not satisfied
 --> src/main.rs:8:14
  |
8 |     for i in v {
  |              ^ `[u32]` is not an iterator; maybe try calling `.iter()` or a similar method
  |
  = help: the trait `std::iter::Iterator` is not implemented for `[u32]`
  = note: required by `std::iter::IntoIterator::into_iter`

Basically, there's no way to dereference a slice. It logically makes sense in any event to just keep a reference to the block of memory holding the values, whether it's on the stack, heap, or the executable itself (like string literals, which we'll get to later).

Deref

There's something fishy; why does the ampersand/borrow operator give us different types? The following compiles just fine!

fn main() {
    let v = vec![1, 2, 3];
    let _: &Vec<u32> = &v;
    let _: &[u32] = &v;
}

It turns out that the borrow operator interacts with "Deref coercion." If you're curious about this, please check out the docs for the Deref trait. As an example, I can create a new struct which can be borrowed into a slice:

use std::ops::Deref;

struct MyArray([u32; 5]);

impl MyArray {
    fn new() -> MyArray {
        MyArray([42; 5])
    }
}

impl Deref for MyArray {
    type Target = [u32];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let ma = MyArray::new();
    let _: &MyArray = &ma;
    let _: &[u32] = &ma;
}

Thanks to udoprog for answering this question. Also, just because you can do this doesn't necessarily mean you should.

Using slices

Slices are data types like any others. You can check out the std::slice module documentation and the slice primitive type.

Some common ways to interact with them include:

  • Using them as Iterators
  • Indexing them with slice[idx] syntax
  • Taking subslices with slice[start..end] syntax

Byte literals

If you put a lower case b in front of a string literal, you'll get a byte array. You can either treat this as a fixed length array or, more commonly, as a slice:

fn main() {
    let bytearray1: &[u8; 22] = b"Hello World in binary!";
    let bytearray2: &[u8] = b"Hello World in binary!";
    println!("{:?}", bytearray1);
    println!("{:?}", bytearray2);
}

Note that you always receive a reference to the value, not the value itself. The data is stored in the program executable itself, and therefore cannot be modified (thus always receiving an immutable reference).

Exercise 4 Add lifetime parameters to the bytearray1 and bytearray2 types above.

Strings

And finally we can talk about strings! You may think that a string literal would be a fixed length array of chars. You can in fact create such a thing:

fn main() {
    let char_array: [char; 5] = ['H', 'e', 'l', 'l', 'o'];
    println!("{:?}", char_array);
}

However, this is not what a str is. The representation above is highly inefficient. Since a char in Rust has full Unicode support, it takes up 4 bytes in memory (32 bits). However, for most data, this is overkill. A character encoding like UTF-8 will be far more efficient.

NOTE If you're not familiar with Unicode and character encodings, it's safe to gloss over these details here. It's not vitally important to understanding how strings work in Rust.

Instead, a string slice (&str) is essentially a newtype wrapper around a byte slice (&[u8]), which is guaranteed to be in UTF-8 encoding. This has some important trade-offs:

  • You can cheaply (freely?) convert from a &str to a &[u8], which can be great for making system calls
  • You cannot get O(1) random access within strings, since the UTF-8 encoding doesn't allow for this. Instead, you need to work with a character iterator to view the individual characters.

Exercise 5 Use std::env::args and the chars() method on String to print out the number of characters in each command line arguments. Bonus points: also print out the number of bytes. Sample usage:

$ cargo run שלום
arg: target/debug/foo, characters: 16, bytes: 16
arg: שלום, characters: 4, bytes: 8

Don't forget, the first argument is the name of the executable.

Lifetimes in data structures

One final topic for today is lifetimes in data structures. It's entirely possible to keep references in your data structures. However, when you do so, you need to be explicit about their lifetimes. For example, this will fail to compile:

struct Person {
    name: &str,
    age: u32,
}

Instead, you would need to write it as:

struct Person<'a> {
    name: &'a str,
    age: u32,
}

The general recommendation I've received, and which I'd pass on, is avoid this when possible. Things end up getting significantly more complicated when dealing with lifetime parameters in data structures. Typically, you should use owned versions of values (e.g. String instead of &str, or Vec or array instead of a slice) inside your data structures. In such a case, you need to ensure that the lifetime of the reference within the structure outlives the structure itself.

There are times when you can avoid some extra cloning and allocation if you use references in your data structure, and the time will probably come when you need to do it. But I'd recommend waiting until your profiling points you at a specific decision being the bottleneck. For more information, see the Rust book.

References and slices in APIs

Some general advice which I received and has mostly steered me correctly is:

When receiving parameters, prefer slices when possible

However, there are times when this is overly simplistic. If you want a deeper dive, there a great blog post covering some trade-offs in public APIs: On dealing with owning and borrowing in public interfaces. The Reddit discussion is also great.

Rust at FP Complete | Introduction

Get new blog posts via email