Contents

Ch.10 — Generic Types, Traits, and Lifetimes

Chapter 10

Generic Types, Traits, and Lifetimes


Every programming language has tools for effectively handling the duplication of concepts. In Rust, one such tool is generics: abstract stand-ins for concrete types or other properties. We can express the behavior of generics, or how they relate to other generics, without knowing what will be in their place when compiling and running the code.

Functions can take parameters of some generic type instead of a concrete type like i32 or String, just as they take parameters with unknown values. We already used generics: Option<T> (Ch.6), Vec<T> and HashMap<K, V> (Ch.8), Result<T, E> (Ch.9). Now you define your own types, functions and methods with generics.

  • Removing Duplication by Extracting a Function
  • 10.1 Generic Data Types
  • 10.2 Defining Shared Behavior with Traits
  • 10.3 Validating References with Lifetimes

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

1

Chapter 10 · Introduction

The road through this chapter

Definition 10.1 (Generics). Abstract stand-ins for concrete types or other properties. Their behavior, and how they relate to other generics, can be expressed without knowing what will be in their place when the code is compiled and run.

  1. First, review how to extract a function to reduce code duplication. Then use the same technique to make a generic function from two functions that differ only in the types of their parameters, and use generic types in struct and enum definitions.
  2. Then, learn how to use traits to define behavior in a generic way. Combining traits with generic types constrains a generic type to accept only those types that have a particular behavior, as opposed to just any type.
  3. Finally, lifetimes: a variety of generics that give the compiler information about how references relate to each other. They let us give the compiler enough information about borrowed values so it can ensure references will be valid in more situations than it could without our help.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

2

Chapter 10 · Removing Duplication by Extracting a Function

From duplicated values to a placeholder

Generics allow us to replace specific types with a placeholder that represents multiple types, to remove code duplication.

Before diving into generics syntax, first look at removing duplication in a way that doesn’t involve generic types: extracting a function that replaces specific values with a placeholder that represents multiple values. Then apply the same technique to extract a generic function!

By learning to recognize duplicated code you can extract into a function, you’ll start to recognize duplicated code that can use generics.

We begin with a short program, Listing 10.1, that finds the largest number in a list.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

3

Chapter 10 · Removing Duplication by Extracting a Function

Finding the largest number in a list

Listing 10.1: Finding the largest number in a list of numbers — src/main.rs

fn main() {

    let number_list = vec![34, 50, 25, 100, 65];

 

    let mut largest = &number_list[0];

 

    for number in &number_list {

        if number > largest {

            largest = number;

        }

    }

 

    println!("The largest number is {largest}");

    assert_eq!(*largest, 100);

}

We store a list of integers in number_list and place a reference to the first number in a variable named largest.

We then iterate through all the numbers: if the current number is greater than the number stored in largest, we replace the reference in that variable. If it is less than or equal to the largest number seen so far, the variable doesn’t change and the code moves on to the next number.

After considering all the numbers, largest should refer to the largest number, which in this case is 100.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

4

Chapter 10 · Removing Duplication by Extracting a Function

Two lists: duplicating the logic

Now we must find the largest number in two different lists. We could duplicate the code of Listing 10.1 and use the same logic at two different places in the program, as in Listing 10.2.

Listing 10.2: Code to find the largest number in two lists of numbers — src/main.rs

fn main() {

    let number_list = vec![34, 50, 25, 100, 65];

 

    let mut largest = &number_list[0];

 

    for number in &number_list {

        if number > largest {

            largest = number;

        }

    }

 

    println!("The largest number is {largest}");

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

5

Chapter 10 · Removing Duplication by Extracting a Function

Two lists: the duplicated second half

Listing 10.2 (continued)

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

 

    let mut largest = &number_list[0];

 

    for number in &number_list {

        if number > largest {

            largest = number;

        }

    }

 

    println!("The largest number is {largest}");

}

Although this code works, duplicating code is tedious and error-prone. We also have to remember to update the code in multiple places when we want to change it.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

6

Chapter 10 · Removing Duplication by Extracting a Function

Extracting a largest function

To eliminate this duplication, we create an abstraction: a function that operates on any list of integers passed in as a parameter. This makes the code clearer and expresses the concept of finding the largest number in a list abstractly.

Listing 10.3 extracts the code into a function named largest and calls it for the two lists from Listing 10.2. We could also use it on any other list of i32 values in the future.

Listing 10.3: Abstracted code to find the largest number in two lists — src/main.rs

fn largest(list: &[i32]) -> &i32 {

    let mut largest = &list[0];

 

    for item in list {

        if item > largest {

            largest = item;

        }

    }

 

    largest

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

7

Chapter 10 · Removing Duplication by Extracting a Function

Calling largest on both lists

Listing 10.3 (continued)

fn main() {

    let number_list = vec![34, 50, 25, 100, 65];

 

    let result = largest(&number_list);

    println!("The largest number is {result}");

    assert_eq!(*result, 100);

 

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

 

    let result = largest(&number_list);

    println!("The largest number is {result}");

    assert_eq!(*result, 6000);

}

The largest function has a parameter called list, which represents any concrete slice of i32 values we might pass into the function.

As a result, when we call the function, the code runs on the specific values that we pass in.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

8

Chapter 10 · Removing Duplication by Extracting a Function

Three steps to remove duplication

In summary, the steps we took to change the code from Listing 10.2 to Listing 10.3:

  1. Identify duplicate code.
  2. Extract the duplicate code into the body of the function, and specify the inputs and return values of that code in the function signature.
  3. Update the two instances of duplicated code to call the function instead.

Next, we use these same steps with generics to reduce code duplication. In the same way that the function body can operate on an abstract list instead of specific values, generics allow code to operate on abstract types.

Example 10.2. Say we had two functions: one that finds the largest item in a slice of i32 values and one that finds the largest item in a slice of char values. How would we eliminate that duplication? Section 10.1 finds out.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

9

Chapter 10 · 10.1 Generic Data Types

Generics in function definitions

We use generics to create definitions for items like function signatures or structs, which we can then use with many different concrete data types. This section first defines functions, structs, enums and methods using generics, then discusses how generics affect code performance.

In function definitions

When defining a function that uses generics, we place the generics in the signature of the function where we would usually specify the data types of the parameters and return value. This makes our code more flexible and provides more functionality to callers of our function, while preventing code duplication.

Continuing with largest, Listing 10.4 shows two functions that both find the largest value in a slice. We’ll then combine these into a single function that uses generics.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

10

Chapter 10 · 10.1 Generic Data Types

Two functions, one body

Listing 10.4: Two functions that differ only in their names and in the types in their signatures — src/main.rs

fn largest_i32(list: &[i32]) -> &i32 {

    let mut largest = &list[0];

 

    for item in list {

        if item > largest {

            largest = item;

        }

    }

 

    largest

}

fn largest_char(list: &[char]) -> &char {

    let mut largest = &list[0];

 

    for item in list {

        if item > largest {

            largest = item;

        }

    }

 

    largest

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

11

Chapter 10 · 10.1 Generic Data Types

Calling largest_i32 and largest_char

Listing 10.4 (continued)

fn main() {

    let number_list = vec![34, 50, 25, 100, 65];

 

    let result = largest_i32(&number_list);

    println!("The largest number is {result}");

    assert_eq!(*result, 100);

 

    let char_list = vec!['y', 'm', 'a', 'q'];

 

    let result = largest_char(&char_list);

    println!("The largest char is {result}");

    assert_eq!(*result, 'y');

}

largest_i32 is the function we extracted in Listing 10.3; it finds the largest i32 in a slice. largest_char finds the largest char in a slice.

The function bodies have the same code, so let’s eliminate the duplication by introducing a generic type parameter in a single function.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

12

Chapter 10 · 10.1 Generic Data Types

Naming and declaring a type parameter

To parameterize the types in a new single function, we name the type parameter, just as we do for the value parameters to a function. Any identifier works, but we use T: by convention, type parameter names in Rust are short, often just one letter, and Rust’s type-naming convention is UpperCamelCase. Short for type, T is the default choice of most Rust programmers.

Definition 10.3 (Generic type parameter). A name, declared in angle brackets <>, that stands for a type in a definition, just as a value parameter stands for a value.

When we use a parameter in a function body, we must declare its name in the signature so the compiler knows what it means. Similarly, a type parameter name used in a function signature must be declared before use: we place type name declarations inside angle brackets, <>, between the function name and the parameter list:

fn largest<T>(list: &[T]) -> &T {

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

13

Chapter 10 · 10.1 Generic Data Types

Reading a generic signature

fn largest<T>(list: &[T]) -> &T {

We read this definition as “The function largest is generic over some type T.”

  • The function has one parameter named list, which is a slice of values of type T.
  • The function will return a reference to a value of the same type T.

Listing 10.5 shows the combined largest function definition using the generic data type in its signature. It also shows how we can call the function with either a slice of i32 values or char values. Note that this code won’t compile yet.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

14

Chapter 10 · 10.1 Generic Data Types

A generic largest — not compiling yet

Listing 10.5: The largest function using generic type parameters; this doesn’t compile yet — src/main.rs

fn largest<T>(list: &[T]) -> &T {

    let mut largest = &list[0];

 

    for item in list {

        if item > largest {

            largest = item;

        }

    }

 

    largest

}

fn main() {

    let number_list = vec![34, 50, 25, 100, 65];

 

    let result = largest(&number_list);

    println!("The largest number is {result}");

 

    let char_list = vec!['y', 'm', 'a', 'q'];

 

    let result = largest(&char_list);

    println!("The largest char is {result}");

}

If we compile this code right now, we get the error shown on the next slide.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

15

Chapter 10 · 10.1 Generic Data Types

Error E0369: > cannot be applied to &T

$ cargo run

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

error[E0369]: binary operation `>` cannot be applied to type `&T`

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

  |

5 |         if item > largest {

  |            ---- ^ ------- &T

  |            |

  |            &T

  |

help: consider restricting type parameter `T` with trait `PartialOrd`

  |

1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {

  |             ++++++++++++++++++++++

 

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

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

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

16

Chapter 10 · 10.1 Generic Data Types

Why it fails: not every T can be compared

The help text mentions std::cmp::PartialOrd, which is a trait — the topic of the next section. For now, know that the error states that the body of largest won’t work for all possible types that T could be.

  • Because we want to compare values of type T in the body, we can only use types whose values can be ordered.
  • To enable comparisons, the standard library has the std::cmp::PartialOrd trait that you can implement on types (more in Appendix C).
  • To fix Listing 10.5, follow the help text’s suggestion: restrict the types valid for T to only those that implement PartialOrd.
  • The listing will then compile, because the standard library implements PartialOrd on both i32 and char.

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

17

Chapter 10 · 10.1 Generic Data Types

Generics in struct definitions

Structs can also use a generic type parameter in one or more fields using the <> syntax. Listing 10.6 defines a Point<T> struct to hold x and y coordinate values of any type.

Listing 10.6: A Point<T> struct that holds x and y values of type T — src/main.rs

struct Point<T> {

    x: T,

    y: T,

}

 

fn main() {

    let integer = Point { x: 5, y: 10 };

    let float = Point { x: 1.0, y: 4.0 };

}

The syntax is similar to that used in function definitions:

  1. Declare the name of the type parameter inside angle brackets just after the name of the struct.
  2. Use the generic type in the struct definition where we would otherwise specify concrete data types.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

18

Chapter 10 · 10.1 Generic Data Types

One type parameter means one type

Because we’ve used only one generic type to define Point<T>, the struct is generic over some type T, and the fields x and y are both that same type, whatever it may be. An instance with values of different types, as in Listing 10.7, won’t compile.

Listing 10.7: The fields x and y must be the same type because both have the same generic data type T. — src/main.rs

struct Point<T> {

    x: T,

    y: T,

}

 

fn main() {

    let wont_work = Point { x: 5, y: 4.0 };

}

Assigning the integer value 5 to x tells the compiler that the generic type T will be an integer for this instance of Point<T>.

Then, when we specify 4.0 for y, which we’ve defined to have the same type as x, we get a type mismatch error.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

19

Chapter 10 · 10.1 Generic Data Types

Error E0308: mismatched types

$ cargo run

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

error[E0308]: mismatched types

 --> src/main.rs:7:38

  |

7 |     let wont_work = Point { x: 5, y: 4.0 };

  |                                      ^^^ expected integer, found floating-point number

 

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

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

To define a Point struct where x and y are both generics but could have different types, we can use multiple generic type parameters. In Listing 10.8, Point becomes generic over types T and U, where x is of type T and y is of type U.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

20

Chapter 10 · 10.1 Generic Data Types

Multiple type parameters: Point<T, U>

Listing 10.8: A Point<T, U> generic over two types so that x and y can be values of different types — src/main.rs

struct Point<T, U> {

    x: T,

    y: U,

}

 

fn main() {

    let both_integer = Point { x: 5, y: 10 };

    let both_float = Point { x: 1.0, y: 4.0 };

    let integer_and_float = Point { x: 5, y: 4.0 };

}

Now all the instances of Point shown are allowed! You can use as many generic type parameters in a definition as you want, but using more than a few makes your code hard to read. If you find you need lots of generic types in your code, it could indicate that your code needs restructuring into smaller pieces.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

21

Chapter 10 · 10.1 Generic Data Types

Generics in enum definitions: Option<T>

As with structs, enums can hold generic data types in their variants. Take another look at the Option<T> enum that the standard library provides, which we used in Chapter 6:

enum Option<T> {

    Some(T),

    None,

}

This definition should now make more sense. Option<T> is generic over type T and has two variants: Some, which holds one value of type T, and None, which doesn’t hold any value.

By using Option<T> we can express the abstract concept of an optional value, and because Option<T> is generic, we can use this abstraction no matter what the type of the optional value is.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

22

Chapter 10 · 10.1 Generic Data Types

Enums with two type parameters: Result<T, E>

Enums can use multiple generic types as well. The definition of the Result enum that we used in Chapter 9 is one example:

enum Result<T, E> {

    Ok(T),

    Err(E),

}

Result is generic over two types, T and E, and has two variants: Ok, which holds a value of type T, and Err, which holds a value of type E. That makes it convenient anywhere an operation might succeed (return a value of some type T) or fail (return an error of some type E). In Listing 9.3, opening a file, T was filled in with std::fs::File on success and E with std::io::Error when there were problems opening the file.

When your code has multiple struct or enum definitions that differ only in the types of the values they hold, you can avoid duplication by using generic types instead.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

23

Chapter 10 · 10.1 Generic Data Types

Generics in method definitions

We can implement methods on structs and enums (Chapter 5) and use generic types in their definitions too. Listing 10.9 shows the Point<T> struct of Listing 10.6 with a method named x implemented on it.

Listing 10.9: Implementing a method named x on the Point<T> struct that will return a reference to the x field of type T — src/main.rs

struct Point<T> {

    x: T,

    y: T,

}

 

impl<T> Point<T> {

    fn x(&self) -> &T {

        &self.x

    }

}

fn main() {

    let p = Point { x: 5, y: 10 };

 

    println!("p.x = {}", p.x());

}

Here, we’ve defined a method named x on Point<T> that returns a reference to the data in the field x.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

24

Chapter 10 · 10.1 Generic Data Types

Declaring T after impl

impl<T> Point<T> {

  • We have to declare T just after impl so that we can use T to specify that we’re implementing methods on the type Point<T>.
  • Declaring T as a generic type after impl lets Rust identify that the type in the angle brackets in Point is a generic type rather than a concrete type.
  • We could choose a different name for this generic parameter than the one declared in the struct definition, but using the same name is conventional.
  • A method written within an impl that declares a generic type is defined on any instance of the type, no matter what concrete type ends up substituting for the generic type.

We can also specify constraints on generic types when defining methods. We could, for example, implement methods only on Point<f32> instances rather than on Point<T> instances with any generic type. Listing 10.10 uses the concrete type f32, meaning we don’t declare any types after impl.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

25

Chapter 10 · 10.1 Generic Data Types

Methods only for Point<f32>

Listing 10.10: An impl block that only applies to a struct with a particular concrete type for the generic type parameter T — src/main.rs

struct Point<T> {

    x: T,

    y: T,

}

 

impl<T> Point<T> {

    fn x(&self) -> &T {

        &self.x

    }

}

impl Point<f32> {

    fn distance_from_origin(&self) -> f32 {

        (self.x.powi(2) + self.y.powi(2)).sqrt()

    }

}

 

fn main() {

    let p = Point { x: 5, y: 10 };

 

    println!("p.x = {}", p.x());

}

Point<f32> will have a distance_from_origin method; instances of Point<T> where T is not f32 will not. The method measures how far our point is from (0.0, 0.0) using mathematical operations available only for floating-point types.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

26

Chapter 10 · 10.1 Generic Data Types

Method generics that differ from the struct’s

Generic type parameters in a struct definition aren’t always the same as those in that struct’s method signatures. Listing 10.11 uses X1 and Y1 for the Point struct and X2 and Y2 for the mixup method.

Listing 10.11: A method that uses generic types that are different from its struct’s definition — src/main.rs

struct Point<X1, Y1> {

    x: X1,

    y: Y1,

}

 

impl<X1, Y1> Point<X1, Y1> {

    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {

        Point {

            x: self.x,

            y: other.y,

        }

    }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

27

Chapter 10 · 10.1 Generic Data Types

Calling mixup

mixup creates a new Point with the x value from the self Point (of type X1) and the y value from the passed-in Point (of type Y2).

Listing 10.11 (continued)

fn main() {

    let p1 = Point { x: 5, y: 10.4 };

    let p2 = Point { x: "Hello", y: 'c' };

 

    let p3 = p1.mixup(p2);

 

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

}

p1 has an i32 for x (value 5) and an f64 for y (value 10.4). p2 has a string slice for x (value "Hello") and a char for y (value c).

Calling mixup on p1 with argument p2 gives p3: an i32 for x (from p1) and a char for y (from p2). It prints p3.x = 5, p3.y = c.

The point: some generic parameters are declared with impl and some with the method definition. X1 and Y1 are declared after impl because they go with the struct definition. X2 and Y2 are declared after fn mixup because they’re only relevant to the method.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

28

Chapter 10 · 10.1 Generic Data Types

Performance of code using generics

Is there a runtime cost to generic type parameters? The good news: using generic types won’t make your program run any slower than it would with concrete types. Rust accomplishes this by performing monomorphization of the code using generics at compile time.

Definition 10.4 (Monomorphization). The process of turning generic code into specific code by filling in the concrete types that are used when compiled.

In this process, the compiler does the opposite of the steps we used to create the generic function in Listing 10.5: it looks at all the places where generic code is called and generates code for the concrete types the generic code is called with.

See how this works with the standard library’s generic Option<T> enum:

let integer = Some(5);

let float = Some(5.0);

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

29

Chapter 10 · 10.1 Generic Data Types

What monomorphized code looks like

When Rust compiles this code, it reads the values used in Option<T> instances and identifies two kinds of Option<T>: one is i32 and the other is f64. It expands the generic definition into two definitions specialized to i32 and f64, replacing the generic definition with the specific ones.

The monomorphized version looks similar to this (the compiler uses different names than these illustrative ones):

src/main.rs

enum Option_i32 {

    Some(i32),

    None,

}

 

enum Option_f64 {

    Some(f64),

    None,

}

fn main() {

    let integer = Option_i32::Some(5);

    let float = Option_f64::Some(5.0);

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

30

Chapter 10 · 10.1 Generic Data Types

No runtime cost — and where generics are declared

The generic Option<T> is replaced with the specific definitions created by the compiler. Because Rust compiles generic code into code that specifies the type in each instance, we pay no runtime cost for using generics: the code performs just as it would if we had duplicated each definition by hand. Monomorphization makes Rust’s generics extremely efficient at runtime.

Table 10.1: Where generic type parameters are declared (Section 10.1 recap)

DeclarationWhere the parameter goes
fn largest<T>(list: &[T]) -> &TAfter the function name, before the parameter list
struct Point<T, U> { x: T, y: U }After the struct name
enum Result<T, E> { Ok(T), Err(E) }After the enum name
impl<T> Point<T> { … }After impl, then used after the type name
impl Point<f32> { … }None: methods only for one concrete type
fn mixup<X2, Y2>(self, …)After the method name, for method-only types

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

31

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Traits and trait bounds

Definition 10.5 (Trait). Defines the functionality a particular type has and can share with other types. Traits define shared behavior in an abstract way.

Definition 10.6 (Trait bound). A constraint specifying that a generic type can be any type that has certain behavior.

Remark. Traits are similar to a feature often called interfaces in other languages, although with some differences.

Defining a trait

A type’s behavior consists of the methods we can call on that type. Different types share the same behavior if we can call the same methods on all of those types. Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

32

Chapter 10 · 10.2 Defining Shared Behavior with Traits

A media aggregator and its Summary trait

Say we have multiple structs that hold various kinds and amounts of text: a NewsArticle struct that holds a news story filed in a particular location, and a SocialPost that can have at most 280 characters, along with metadata indicating whether it was a new post, a repost, or a reply to another post.

We want a media aggregator library crate named aggregator that can display summaries of data that might be stored in a NewsArticle or SocialPost instance. We need a summary from each type, requested by calling a summarize method on an instance. Listing 10.12 defines a public Summary trait that expresses this behavior.

Listing 10.12: A Summary trait that consists of the behavior provided by a summarize method — src/lib.rs

pub trait Summary {

    fn summarize(&self) -> String;

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

33

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Anatomy of a trait definition

  • We declare a trait using the trait keyword and then the trait’s name, here Summary.
  • We declare the trait as pub so that crates depending on this crate can make use of this trait too.
  • Inside the curly brackets, we declare the method signatures that describe the behaviors of the types that implement this trait, here fn summarize(&self) -> String.
  • After the method signature, instead of providing an implementation within curly brackets, we use a semicolon. Each type implementing this trait must provide its own custom behavior for the body of the method.
  • The compiler enforces that any type that has the Summary trait will have the method summarize defined with this signature exactly.
  • A trait can have multiple methods in its body: the method signatures are listed one per line, and each line ends in a semicolon.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

34

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Implementing a trait on a type

Listing 10.13 implements Summary on NewsArticle, using the headline, the author and the location to build the return value of summarize. For SocialPost, summarize is the username followed by the entire text of the post, assuming the content is already limited to 280 characters.

Listing 10.13: Implementing the Summary trait on the NewsArticle and SocialPost types — src/lib.rs

pub trait Summary {

    fn summarize(&self) -> String;

}

 

pub struct NewsArticle {

    pub headline: String,

    pub location: String,

    pub author: String,

    pub content: String,

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

35

Chapter 10 · 10.2 Defining Shared Behavior with Traits

impl Summary for NewsArticle

Listing 10.13 (continued)

impl Summary for NewsArticle {

    fn summarize(&self) -> String {

        format!("{}, by {} ({})", self.headline, self.author, self.location)

    }

}

 

pub struct SocialPost {

    pub username: String,

    pub content: String,

    pub reply: bool,

    pub repost: bool,

}

Implementing a trait on a type is similar to implementing regular methods. The difference is that after impl, we put the trait name we want to implement, then the for keyword, and then the name of the type we want to implement the trait for.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

36

Chapter 10 · 10.2 Defining Shared Behavior with Traits

impl Summary for SocialPost

Listing 10.13 (continued)

impl Summary for SocialPost {

    fn summarize(&self) -> String {

        format!("{}: {}", self.username, self.content)

    }

}

Within the impl block, we put the method signatures that the trait definition has defined. Instead of adding a semicolon after each signature, we use curly brackets and fill in the method body with the specific behavior that we want the methods of the trait to have for the particular type.

Now that the library has implemented Summary on NewsArticle and SocialPost, users of the crate can call the trait methods on instances of these types in the same way we call regular methods. The only difference is that the user must bring the trait into scope as well as the types.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

37

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Using the trait from a binary crate

A binary crate using our aggregator library brings Summary into scope along with SocialPost:

use aggregator::{SocialPost, Summary};

 

fn main() {

    let post = SocialPost {

        username: String::from("horse_ebooks"),

        content: String::from(

            "of course, as you probably already know, people",

        ),

        reply: false,

        repost: false,

    };

 

    println!("1 new post: {}", post.summarize());

}

This code prints 1 new post: horse_ebooks: of course, as you probably already know, people.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

38

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Who may implement what: the orphan rule

Other crates that depend on aggregator can also bring Summary into scope to implement it on their own types. One restriction: we can implement a trait on a type only if either the trait or the type, or both, are local to our crate.

Allowed

  • Standard library trait Display on our type SocialPost: the type is local to aggregator.
  • Our trait Summary on Vec<T>: the trait is local to aggregator.

Not allowed

  • External traits on external types: Display on Vec<T> within aggregator, because both are defined in the standard library.

Definition 10.7 (Orphan rule). Part of a property called coherence: external traits can’t be implemented on external types — so named because the parent type is not present. It ensures other people’s code can’t break your code and vice versa. Without it, two crates could implement the same trait for the same type, and Rust wouldn’t know which implementation to use.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

39

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Using default implementations

Sometimes it’s useful to have default behavior for some or all of the methods in a trait instead of requiring implementations for all methods on every type. Then, as we implement the trait on a particular type, we can keep or override each method’s default behavior.

Definition 10.8 (Default implementation). A method body written in the trait definition itself, used by every implementing type that does not override it.

In Listing 10.14, we specify a default string for the summarize method of the Summary trait instead of only defining the method signature, as we did in Listing 10.12.

To use a default implementation to summarize instances of NewsArticle, we specify an empty impl block: impl Summary for NewsArticle {}.

We no longer define summarize on NewsArticle directly, but we provided a default and said that NewsArticle implements Summary, so we can still call summarize on an instance:

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

40

Chapter 10 · 10.2 Defining Shared Behavior with Traits

A default summarize, and an empty impl

Listing 10.14: Defining a Summary trait with a default implementation of the summarize method — src/lib.rs

pub trait Summary {

    fn summarize(&self) -> String {

        String::from("(Read more...)")

    }

}

 

pub struct NewsArticle {

    pub headline: String,

    pub location: String,

    pub author: String,

    pub content: String,

}

 

impl Summary for NewsArticle {}

pub struct SocialPost {

    pub username: String,

    pub content: String,

    pub reply: bool,

    pub repost: bool,

}

 

impl Summary for SocialPost {

    fn summarize(&self) -> String {

        format!("{}: {}", self.username, self.content)

    }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

41

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Calling the default implementation

use aggregator::{self, NewsArticle, Summary};

 

fn main() {

    let article = NewsArticle {

        headline: String::from("Penguins win the Stanley Cup Championship!"),

        location: String::from("Pittsburgh, PA, USA"),

        author: String::from("Iceburgh"),

        content: String::from(

            "The Pittsburgh Penguins once again are the best \

             hockey team in the NHL.",

        ),

    };

 

    println!("New article available! {}", article.summarize());

}

This code prints New article available! (Read more...).

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

42

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Defaults that call other trait methods

Creating a default implementation doesn’t require changing the implementation of Summary on SocialPost in Listing 10.13: the syntax for overriding a default implementation is the same as the syntax for implementing a trait method that doesn’t have one.

Default implementations can call other methods in the same trait, even if those methods don’t have a default implementation. A trait can thus provide a lot of useful functionality and require implementors to specify only a small part of it. For example, Summary could have a required summarize_author method, and a summarize method whose default implementation calls it:

pub trait Summary {

    fn summarize_author(&self) -> String;

 

    fn summarize(&self) -> String {

        format!("(Read more from {}...)", self.summarize_author())

    }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

43

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Implementing only summarize_author

To use this version of Summary, we only need to define summarize_author when we implement the trait on a type:

impl Summary for SocialPost {

    fn summarize_author(&self) -> String {

        format!("@{}", self.username)

    }

}

After we define summarize_author, we can call summarize on instances of SocialPost, and the default implementation of summarize will call the definition of summarize_author that we’ve provided.

Because we’ve implemented summarize_author, the Summary trait has given us the behavior of the summarize method without requiring us to write any more code.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

44

Chapter 10 · 10.2 Defining Shared Behavior with Traits

The default summarize in action

use aggregator::{self, SocialPost, Summary};

 

fn main() {

    let post = SocialPost {

        username: String::from("horse_ebooks"),

        content: String::from(

            "of course, as you probably already know, people",

        ),

        reply: false,

        repost: false,

    };

 

    println!("1 new post: {}", post.summarize());

}

This code prints 1 new post: (Read more from @horse_ebooks...).

Note that it isn’t possible to call the default implementation from an overriding implementation of that same method.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

45

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Using traits as parameters: impl Trait

Now we can use traits to define functions that accept many different types. Using the Summary trait implemented on NewsArticle and SocialPost in Listing 10.13, we define a notify function that calls summarize on its item parameter, which is of some type that implements Summary. We use the impl Trait syntax:

pub fn notify(item: &impl Summary) {

    println!("Breaking news! {}", item.summarize());

}

  • Instead of a concrete type for item, we specify the impl keyword and the trait name. The parameter accepts any type that implements the specified trait.
  • In the body of notify, we can call any methods on item that come from Summary, such as summarize.
  • We can pass in any instance of NewsArticle or SocialPost. Calling it with any other type, such as a String or an i32, won’t compile, because those types don’t implement Summary.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

46

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Trait bound syntax

The impl Trait syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound:

pub fn notify<T: Summary>(item: &T) {

    println!("Breaking news! {}", item.summarize());

}

This longer form is equivalent to the previous example but more verbose. We place trait bounds with the declaration of the generic type parameter, after a colon and inside angle brackets.

impl Trait is convenient and makes for more concise code in simple cases, while the fuller trait bound syntax can express more complexity in other cases. For example, we can have two parameters that implement Summary; with impl Trait that looks like this:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

47

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Different types, or the same type?

impl Trait: the types may differ

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Using impl Trait is appropriate if we want this function to allow item1 and item2 to have different types (as long as both types implement Summary).

Trait bound: one shared type

pub fn notify<T: Summary>(item1: &T, item2: &T) {

To force both parameters to have the same type, we must use a trait bound. The generic type T, specified as the type of item1 and item2, constrains the function so that the concrete type of the value passed as an argument for item1 and item2 must be the same.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

48

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Multiple trait bounds with the + syntax

We can also specify more than one trait bound. Say we wanted notify to use display formatting as well as summarize on item: we specify in the notify definition that item must implement both Display and Summary, using the + syntax:

pub fn notify(item: &(impl Summary + Display)) {

The + syntax is also valid with trait bounds on generic types:

pub fn notify<T: Summary + Display>(item: &T) {

With the two trait bounds specified, the body of notify can call summarize and use {} to format item.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

49

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Clearer trait bounds with where clauses

Too many trait bounds have downsides. Each generic has its own trait bounds, so functions with multiple generic type parameters can hold lots of trait bound information between the function’s name and its parameter list, making the signature hard to read. Rust therefore has an alternate syntax: trait bounds inside a where clause after the function signature. Instead of writing this:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

we can use a where clause, like this:

fn some_function<T, U>(t: &T, u: &U) -> i32

where

    T: Display + Clone,

    U: Clone + Debug,

{

    unimplemented!()

}

This function’s signature is less cluttered: the function name, parameter list and return type are close together, similar to a function without lots of trait bounds.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

50

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Returning types that implement traits

We can also use impl Trait in the return position, to return a value of some type that implements a trait:

fn returns_summarizable() -> impl Summary {

    SocialPost {

        username: String::from("horse_ebooks"),

        content: String::from(

            "of course, as you probably already know, people",

        ),

        reply: false,

        repost: false,

    }

}

By using impl Summary for the return type, we specify that returns_summarizable returns some type that implements the Summary trait without naming the concrete type. Here it returns a SocialPost, but the code calling this function doesn’t need to know that.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

51

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Where returning impl Trait shines — and its limit

Specifying a return type only by the trait it implements is especially useful with closures and iterators (Chapter 13). Closures and iterators create types that only the compiler knows, or types that are very long to specify. The impl Trait syntax lets you concisely say that a function returns some type that implements the Iterator trait without writing out a very long type.

Remark. You can only use impl Trait if you’re returning a single type.

For example, the code on the next two slides, which returns either a NewsArticle or a SocialPost with the return type specified as impl Summary, wouldn’t work.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

52

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Two concrete types behind impl Summary: rejected

This function tries to return a NewsArticle in one branch …

fn returns_summarizable(switch: bool) -> impl Summary {

    if switch {

        NewsArticle {

            headline: String::from(

                "Penguins win the Stanley Cup Championship!",

            ),

            location: String::from("Pittsburgh, PA, USA"),

            author: String::from("Iceburgh"),

            content: String::from(

                "The Pittsburgh Penguins once again are the best \

                 hockey team in the NHL.",

            ),

        }

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

53

Chapter 10 · 10.2 Defining Shared Behavior with Traits

… and a SocialPost in the other

    } else {

        SocialPost {

            username: String::from("horse_ebooks"),

            content: String::from(

                "of course, as you probably already know, people",

            ),

            reply: false,

            repost: false,

        }

    }

}

Returning either a NewsArticle or a SocialPost isn’t allowed, due to restrictions around how the impl Trait syntax is implemented in the compiler. How to write a function with this behavior is covered in “Using Trait Objects to Abstract over Shared Behavior” in Chapter 18.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

54

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Trait bounds that conditionally implement methods

By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.

Example 10.9. In Listing 10.15, the type Pair<T> always implements the new function to return a new instance of Pair<T>. But in the next impl block, Pair<T> only implements the cmp_display method if its inner type T implements the PartialOrd trait that enables comparison and the Display trait that enables printing.

Remark. Recall from “Method Syntax” in Chapter 5 that Self is a type alias for the type of the impl block, which in this case is Pair<T>.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

55

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Pair<T>: new always, cmp_display conditionally

Listing 10.15: Conditionally implementing methods on a generic type depending on trait bounds — src/lib.rs

use std::fmt::Display;

 

struct Pair<T> {

    x: T,

    y: T,

}

 

impl<T> Pair<T> {

    fn new(x: T, y: T) -> Self {

        Self { x, y }

    }

}

impl<T: Display + PartialOrd> Pair<T> {

    fn cmp_display(&self) {

        if self.x >= self.y {

            println!("The largest member is x = {}", self.x);

        } else {

            println!("The largest member is y = {}", self.y);

        }

    }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

56

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Blanket implementations

We can also conditionally implement a trait for any type that implements another trait. Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations, and they are used extensively in the Rust standard library. For example, the standard library implements ToString on any type that implements Display; its impl block looks similar to this:

impl<T: Display> ToString for T {

    // --snip--

}

Because of this blanket implementation, we can call the to_string method defined by ToString on any type that implements Display. For example, we can turn integers into their corresponding String values because integers implement Display:

let s = 3.to_string();

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

57

Chapter 10 · 10.2 Defining Shared Behavior with Traits

Errors at compile time, not at runtime

Blanket implementations appear in the documentation for the trait in the “Implementors” section.

Traits and trait bounds let us write code that uses generic type parameters to reduce duplication, but also specify to the compiler that we want the generic type to have particular behavior. The compiler can then use the trait bound information to check that all the concrete types used with our code provide the correct behavior.

  • In dynamically typed languages, we would get an error at runtime if we called a method on a type that didn’t define the method.
  • Rust moves these errors to compile time, so we’re forced to fix the problems before our code is even able to run.
  • We don’t have to write code that checks for behavior at runtime, because we’ve already checked at compile time. This improves performance without giving up the flexibility of generics.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

58

Chapter 10 · 10.3 Validating References with Lifetimes

Lifetimes: another kind of generic

Lifetimes are another kind of generic that we’ve already been using. Rather than ensuring that a type has the behavior we want, lifetimes ensure that references are valid as long as we need them to be.

Definition 10.10 (Lifetime). The scope for which a reference is valid. Every reference in Rust has a lifetime — a detail not discussed in “References and Borrowing” (Chapter 4).

Most of the time, lifetimes are implicit and inferred, just as most of the time types are inferred. We must annotate types only when multiple types are possible; similarly, we must annotate lifetimes when the lifetimes of references could be related in a few different ways. Rust requires us to annotate the relationships using generic lifetime parameters to ensure that the actual references used at runtime will definitely be valid.

Remark. Annotating lifetimes is not even a concept most other programming languages have, so this will feel unfamiliar. The chapter doesn’t cover lifetimes in their entirety, but discusses common ways you might encounter lifetime syntax, so you can get comfortable with the concept.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

59

Chapter 10 · 10.3 Validating References with Lifetimes

Dangling references

The main aim of lifetimes is to prevent dangling references, which, if they were allowed to exist, would cause a program to reference data other than the data it’s intended to reference. Consider Listing 10.16, which has an outer scope and an inner scope.

Listing 10.16: An attempt to use a reference whose value has gone out of scope

fn main() {

    let r;

 

    {

        let x = 5;

        r = &x;

    }

 

    println!("r: {r}");

}

Remark. Listings 10.16, 10.17 and 10.23 declare variables without giving them an initial value, so the variable name exists in the outer scope. At first glance this might seem to conflict with Rust having no null values. However, if we try to use a variable before giving it a value, we get a compile-time error, which shows that Rust indeed does not allow null values.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

60

Chapter 10 · 10.3 Validating References with Lifetimes

What goes wrong in Listing 10.16

Definition 10.11 (Dangling reference). A reference that would point to data other than the data it’s intended to reference, because the referenced value has gone out of scope.

  • The outer scope declares a variable named r with no initial value.
  • The inner scope declares a variable named x with the initial value 5, and sets the value of r as a reference to x.
  • Then the inner scope ends, and we attempt to print the value in r.

This code won’t compile, because the value that r is referring to has gone out of scope before we try to use it. The error message is on the next slide.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

61

Chapter 10 · 10.3 Validating References with Lifetimes

Error E0597: `x` does not live long enough

$ cargo run

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

error[E0597]: `x` does not live long enough

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

  |

5 |         let x = 5;

  |             - binding `x` declared here

6 |         r = &x;

  |             ^^ borrowed value does not live long enough

7 |     }

  |     - `x` dropped here while still borrowed

8 |

9 |     println!("r: {r}");

  |                   - borrow later used here

 

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

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

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

62

Chapter 10 · 10.3 Validating References with Lifetimes

Reading the error, and the borrow checker

The error says that x “does not live long enough.” x will be out of scope when the inner scope ends on line 7, but r is still valid for the outer scope; because its scope is larger, we say that it “lives longer.” If Rust allowed this code, r would be referencing memory that was deallocated when x went out of scope, and anything we tried to do with r wouldn’t work correctly. So, how does Rust determine that this code is invalid? It uses a borrow checker.

The borrow checker

Definition 10.12 (Borrow checker). The part of the Rust compiler that compares scopes to determine whether all borrows are valid.

Listing 10.17 shows the same code as Listing 10.16, but with annotations showing the lifetimes of the variables.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

63

Chapter 10 · 10.3 Validating References with Lifetimes

Comparing the lifetimes 'a and 'b

Listing 10.17: Annotations of the lifetimes of r and x, named 'a and 'b, respectively

fn main() {

    let r;                // ---------+-- 'a

                          //          |

    {                     //          |

        let x = 5;        // -+-- 'b  |

        r = &x;           //  |       |

    }                     // -+       |

                          //          |

    println!("r: {r}");   //          |

}                         // ---------+

We’ve annotated the lifetime of r with 'a and the lifetime of x with 'b. The inner 'b block is much smaller than the outer 'a lifetime block.

At compile time, Rust compares the size of the two lifetimes and sees that r has a lifetime of 'a but refers to memory with a lifetime of 'b.

The program is rejected because 'b is shorter than 'a: the subject of the reference doesn’t live as long as the reference.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

64

Chapter 10 · 10.3 Validating References with Lifetimes

A valid reference: the data outlives it

Listing 10.18 fixes the code so that it doesn’t have a dangling reference, and it compiles without any errors.

Listing 10.18: A valid reference because the data has a longer lifetime than the reference

fn main() {

    let x = 5;            // ----------+-- 'b

                          //           |

    let r = &x;           // --+-- 'a  |

                          //   |       |

    println!("r: {r}");   //   |       |

                          // --+       |

}                         // ----------+

Here, x has the lifetime 'b, which in this case is larger than 'a. This means r can reference x, because Rust knows that the reference in r will always be valid while x is valid.

Now that you know where the lifetimes of references are and how Rust analyzes lifetimes to ensure that references will always be valid, let’s explore generic lifetimes in function parameters and return values.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

65

Chapter 10 · 10.3 Validating References with Lifetimes

Generic lifetimes in functions

We’ll write a function that returns the longer of two string slices: it takes two string slices and returns a single string slice. After we’ve implemented longest, the code in Listing 10.19 should print The longest string is abcd.

Listing 10.19: A main function that calls the longest function to find the longer of two string slices — src/main.rs

fn main() {

    let string1 = String::from("abcd");

    let string2 = "xyz";

 

    let result = longest(string1.as_str(), string2);

    println!("The longest string is {result}");

}

We want the function to take string slices, which are references, rather than strings, because we don’t want longest to take ownership of its parameters. See “String Slices as Parameters” in Chapter 4 for why these are the parameters we want.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

66

Chapter 10 · 10.3 Validating References with Lifetimes

A first longest that doesn’t compile

If we try to implement the longest function as shown in Listing 10.20, it won’t compile.

Listing 10.20: An implementation of the longest function that returns the longer of two string slices but does not yet compile — src/main.rs

fn main() {

    let string1 = String::from("abcd");

    let string2 = "xyz";

 

    let result = longest(string1.as_str(), string2);

    println!("The longest string is {result}");

}

 

fn longest(x: &str, y: &str) -> &str {

    if x.len() > y.len() { x } else { y }

}

Instead, we get an error that talks about lifetimes (next slide).

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

67

Chapter 10 · 10.3 Validating References with Lifetimes

Error E0106: missing lifetime specifier

$ cargo run

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

error[E0106]: missing lifetime specifier

 --> src/main.rs:9:33

  |

9 | fn longest(x: &str, y: &str) -> &str {

  |               ----     ----     ^ expected named lifetime parameter

  |

  = help: this function's return type contains a borrowed value,

          but the signature does not say whether it is borrowed from `x` or `y`

help: consider introducing a named lifetime parameter

  |

9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

  |           ++++     ++          ++          ++

 

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

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

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

68

Chapter 10 · 10.3 Validating References with Lifetimes

Why the borrow checker can’t decide

The help text reveals that the return type needs a generic lifetime parameter on it, because Rust can’t tell whether the reference being returned refers to x or y. Actually, we don’t know either: the if block in the body returns a reference to x and the else block returns a reference to y!

  • When defining this function, we don’t know the concrete values that will be passed in, so we don’t know whether the if case or the else case will execute.
  • We also don’t know the concrete lifetimes of the references that will be passed in, so we can’t look at the scopes as in Listings 10.17 and 10.18 to determine whether the reference we return will always be valid.
  • The borrow checker can’t determine this either, because it doesn’t know how the lifetimes of x and y relate to the lifetime of the return value.

To fix this error, we add generic lifetime parameters that define the relationship between the references, so the borrow checker can perform its analysis.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

69

Chapter 10 · 10.3 Validating References with Lifetimes

Lifetime annotation syntax

Definition 10.13 (Lifetime annotation). A generic lifetime parameter, such as 'a, written on references. It describes the relationships of the lifetimes of multiple references to each other, without affecting the lifetimes.

Lifetime annotations don’t change how long any of the references live. Just as functions can accept any type when the signature specifies a generic type parameter, functions can accept references with any lifetime by specifying a generic lifetime parameter.

  • Names of lifetime parameters must start with an apostrophe (') and are usually all lowercase and very short, like generic types.
  • Most people use the name 'a for the first lifetime annotation.
  • Lifetime parameter annotations go after the & of a reference, with a space separating the annotation from the reference’s type.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

70

Chapter 10 · 10.3 Validating References with Lifetimes

Annotated references, and annotations in signatures

Examples: a reference to an i32 without a lifetime parameter, a reference to an i32 with a lifetime parameter named 'a, and a mutable reference to an i32 that also has the lifetime 'a:

&i32        // a reference

&'a i32     // a reference with an explicit lifetime

&'a mut i32 // a mutable reference with an explicit lifetime

One lifetime annotation by itself doesn’t have much meaning: annotations are meant to tell Rust how generic lifetime parameters of multiple references relate to each other. Let’s examine that in the context of longest.

In function signatures

To use lifetime annotations in function signatures, we declare the generic lifetime parameters inside angle brackets between the function name and the parameter list, just as we did with generic type parameters.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

71

Chapter 10 · 10.3 Validating References with Lifetimes

longest with a lifetime 'a

Listing 10.21: The longest function definition specifying that all the references in the signature must have the same lifetime 'a — src/main.rs

fn main() {

    let string1 = String::from("abcd");

    let string2 = "xyz";

 

    let result = longest(string1.as_str(), string2);

    println!("The longest string is {result}");

}

 

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

    if x.len() > y.len() { x } else { y }

}

We want the signature to express this constraint: the returned reference will be valid as long as both of the parameters are valid. That is the relationship between the lifetimes of the parameters and the return value. We name the lifetime 'a and add it to each reference.

This code should compile and produce the result we want when we use it with the main function in Listing 10.19.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

72

Chapter 10 · 10.3 Validating References with Lifetimes

What the signature tells Rust

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

  • For some lifetime 'a, the function takes two parameters, both string slices that live at least as long as lifetime 'a.
  • The string slice returned from the function will live at least as long as lifetime 'a.
  • In practice: the lifetime of the reference returned by longest is the same as the smaller of the lifetimes of the values referred to by the function arguments. These relationships are what we want Rust to use when analyzing this code.

Specifying lifetime parameters in this signature does not change the lifetimes of any values passed in or returned. Rather, we specify that the borrow checker should reject any values that don’t adhere to these constraints. longest doesn’t need to know exactly how long x and y will live, only that some scope can be substituted for 'a that will satisfy this signature.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

73

Chapter 10 · 10.3 Validating References with Lifetimes

Annotations as a contract

When annotating lifetimes in functions, the annotations go in the function signature, not in the function body. They become part of the contract of the function, much like the types in the signature.

  • Having signatures contain the lifetime contract makes the analysis the Rust compiler does simpler.
  • If there’s a problem with the way a function is annotated or called, the compiler errors can point to the part of our code and the constraints more precisely.
  • If, instead, the compiler made more inferences about what we intended the relationships of the lifetimes to be, it might only be able to point to a use of our code many steps away from the cause of the problem.

When we pass concrete references to longest, the concrete lifetime substituted for 'a is the part of the scope of x that overlaps with the scope of y: the generic lifetime 'a gets the concrete lifetime equal to the smaller of the lifetimes of x and y. Because the returned reference has the same lifetime parameter 'a, it is also valid for the length of the smaller of those lifetimes.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

74

Chapter 10 · 10.3 Validating References with Lifetimes

Different concrete lifetimes: accepted

Let’s see how the annotations restrict longest by passing in references that have different concrete lifetimes. Listing 10.22 is a straightforward example.

Listing 10.22: Using the longest function with references to String values that have different concrete lifetimes — src/main.rs

fn main() {

    let string1 = String::from("long string is long");

 

    {

        let string2 = String::from("xyz");

        let result = longest(string1.as_str(), string2.as_str());

        println!("The longest string is {result}");

    }

}

 

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

    if x.len() > y.len() { x } else { y }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

75

Chapter 10 · 10.3 Validating References with Lifetimes

Why 10.22 passes, and an experiment that fails

  • string1 is valid until the end of the outer scope.
  • string2 is valid until the end of the inner scope.
  • result references something that is valid until the end of the inner scope.

Run this code and the borrow checker approves; it compiles and prints The longest string is long string is long.

Next, an example showing that the lifetime of the reference in result must be the smaller lifetime of the two arguments:

  1. Move the declaration of result outside the inner scope, but leave the assignment to result inside the scope with string2.
  2. Move the println! that uses result outside the inner scope, after it has ended.

The code in Listing 10.23 will not compile.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

76

Chapter 10 · 10.3 Validating References with Lifetimes

Using result after string2 is gone

Listing 10.23: Attempting to use result after string2 has gone out of scope — src/main.rs

fn main() {

    let string1 = String::from("long string is long");

    let result;

    {

        let string2 = String::from("xyz");

        result = longest(string1.as_str(), string2.as_str());

    }

    println!("The longest string is {result}");

}

 

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

    if x.len() > y.len() { x } else { y }

}

When we try to compile this code, we get the error on the next slide.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

77

Chapter 10 · 10.3 Validating References with Lifetimes

Error E0597: `string2` does not live long enough

$ cargo run

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

error[E0597]: `string2` does not live long enough

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

  |

5 |         let string2 = String::from("xyz");

  |             ------- binding `string2` declared here

6 |         result = longest(string1.as_str(), string2.as_str());

  |                                            ^^^^^^^ borrowed value does not live long enough

7 |     }

  |     - `string2` dropped here while still borrowed

8 |     println!("The longest string is {result}");

  |                                      ------ borrow later used here

 

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

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

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

78

Chapter 10 · 10.3 Validating References with Lifetimes

The compiler goes by the signature

The error shows that for result to be valid for the println! statement, string2 would need to be valid until the end of the outer scope. Rust knows this because we annotated the lifetimes of the function parameters and return values using the same lifetime parameter 'a.

As humans, we can see that string1 is longer than string2, so result will contain a reference to string1; and because string1 has not gone out of scope yet, that reference will still be valid for the println!. But the compiler can’t see that the reference is valid in this case. We’ve told Rust that the lifetime of the reference returned by longest is the same as the smaller of the lifetimes of the references passed in. Therefore, the borrow checker disallows Listing 10.23 as possibly having an invalid reference.

Example 10.14. Design more experiments that vary the values and lifetimes of the references passed in to longest and how the returned reference is used. Make hypotheses about whether your experiments will pass the borrow checker before you compile; then check whether you’re right!

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

79

Chapter 10 · 10.3 Validating References with Lifetimes

Relationships: annotate only what is related

The way you need to specify lifetime parameters depends on what your function is doing. If we changed longest to always return the first parameter rather than the longest string slice, we wouldn’t need to specify a lifetime on the y parameter. The following code compiles:

src/main.rs

fn main() {

    let string1 = String::from("abcd");

    let string2 = "efghijklmnopqrstuvwxyz";

 

    let result = longest(string1.as_str(), string2);

    println!("The longest string is {result}");

}

 

fn longest<'a>(x: &'a str, y: &str) -> &'a str {

    x

}

We’ve specified a lifetime parameter 'a for the parameter x and the return type, but not for the parameter y, because the lifetime of y does not have any relationship with the lifetime of x or the return value.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

80

Chapter 10 · 10.3 Validating References with Lifetimes

Returning a reference to a local value

When returning a reference from a function, the lifetime parameter for the return type needs to match the lifetime parameter for one of the parameters. If the returned reference does not refer to one of the parameters, it must refer to a value created within this function — a dangling reference, because that value goes out of scope at the end of the function. This attempted longest won’t compile:

fn main() {

    let string1 = String::from("abcd");

    let string2 = "xyz";

 

    let result = longest(string1.as_str(), string2);

    println!("The longest string is {result}");

}

 

fn longest<'a>(x: &str, y: &str) -> &'a str {

    let result = String::from("really long string");

    result.as_str()

}

Even though we’ve specified a lifetime parameter 'a for the return type, this implementation fails to compile, because the return value lifetime is not related to the lifetime of the parameters at all.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

81

Chapter 10 · 10.3 Validating References with Lifetimes

Error E0515: returning a reference to a local

Here is the error message we get:

$ cargo run

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

error[E0515]: cannot return value referencing local variable `result`

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

   |

11 |     result.as_str()

   |     ------^^^^^^^^^

   |     |

   |     returns a value referencing data owned by the current function

   |     `result` is borrowed here

 

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

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

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

82

Chapter 10 · 10.3 Validating References with Lifetimes

Fix: return owned data

The problem is that result goes out of scope and gets cleaned up at the end of the longest function, while we’re also trying to return a reference to result from the function.

  • There is no way to specify lifetime parameters that would change the dangling reference, and Rust won’t let us create a dangling reference.
  • The best fix here is to return an owned data type rather than a reference, so that the calling function is then responsible for cleaning up the value.

Ultimately, lifetime syntax is about connecting the lifetimes of various parameters and return values of functions. Once they’re connected, Rust has enough information to allow memory-safe operations and disallow operations that would create dangling pointers or otherwise violate memory safety.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

83

Chapter 10 · 10.3 Validating References with Lifetimes

Lifetimes in struct definitions

So far, the structs we’ve defined all hold owned types. Structs can hold references, but then we need a lifetime annotation on every reference in the struct’s definition. Listing 10.24 has a struct named ImportantExcerpt that holds a string slice.

Listing 10.24: A struct that holds a reference, requiring a lifetime annotation — src/main.rs

struct ImportantExcerpt<'a> {

    part: &'a str,

}

 

fn main() {

    let novel = String::from("Call me Ishmael. Some years ago...");

    let first_sentence = novel.split('.').next().unwrap();

    let i = ImportantExcerpt {

        part: first_sentence,

    };

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

84

Chapter 10 · 10.3 Validating References with Lifetimes

An instance can’t outlive its reference

struct ImportantExcerpt<'a> {

    part: &'a str,

}

This struct has the single field part, which holds a string slice — a reference. As with generic data types, we declare the name of the generic lifetime parameter inside angle brackets after the name of the struct, so we can use it in the body of the struct definition. This annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.

The main function creates an instance of ImportantExcerpt that holds a reference to the first sentence of the String owned by the variable novel. The data in novel exists before the ImportantExcerpt instance is created, and novel doesn’t go out of scope until after the ImportantExcerpt goes out of scope, so the reference in the instance is valid.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

85

Chapter 10 · 10.3 Validating References with Lifetimes

Lifetime elision: references without annotations

Every reference has a lifetime, and functions or structs that use references need lifetime parameters. Yet the function from Listing 4.9, shown again in Listing 10.25, compiled without lifetime annotations.

Listing 10.25: A function we defined in Listing 4.9 that compiled without lifetime annotations, even though the parameter and return type are references — src/lib.rs

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.10 Generic Types, Traits, and Lifetimes

86

Chapter 10 · 10.3 Validating References with Lifetimes

Calling first_word

Listing 10.25 (continued)

fn main() {

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

 

    // first_word works on slices of `String`s

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

 

    let my_string_literal = "hello world";

 

    // first_word works on slices of string literals

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

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

87

Chapter 10 · 10.3 Validating References with Lifetimes

A bit of history: why no annotations are needed

The reason this function compiles without lifetime annotations is historical. In early versions (pre-1.0) of Rust, this code wouldn’t have compiled, because every reference needed an explicit lifetime. The signature would have been written like this:

fn first_word<'a>(s: &'a str) -> &'a str {

After writing a lot of Rust code, the Rust team found that Rust programmers were entering the same lifetime annotations over and over in particular situations. These situations were predictable and followed a few deterministic patterns. The developers programmed these patterns into the compiler’s code so the borrow checker could infer the lifetimes in these situations without explicit annotations.

This history matters because more deterministic patterns may emerge and be added to the compiler. In the future, even fewer lifetime annotations might be required.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

88

Chapter 10 · 10.3 Validating References with Lifetimes

Elision rules, input and output lifetimes

Definition 10.15 (Lifetime elision rules). The patterns programmed into Rust’s analysis of references: a set of particular cases the compiler considers. If your code fits these cases, you don’t need to write the lifetimes explicitly.

They aren’t rules for programmers to follow. And they don’t provide full inference: if there is still ambiguity about what lifetimes the references have after Rust applies the rules, the compiler won’t guess what the lifetime of the remaining references should be. Instead of guessing, it gives you an error that you can resolve by adding the lifetime annotations.

Definition 10.16 (Input and output lifetimes). Lifetimes on function or method parameters are input lifetimes; lifetimes on return values are output lifetimes.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

89

Chapter 10 · 10.3 Validating References with Lifetimes

The three lifetime elision rules

Without explicit annotations, the compiler uses three rules to figure out the lifetimes of references. The first applies to input lifetimes; the second and third apply to output lifetimes. If the compiler gets to the end of the three rules and there are still references whose lifetimes it can’t figure out, it stops with an error. The rules apply to fn definitions as well as impl blocks.

  1. The compiler assigns a lifetime parameter to each parameter that’s a reference: one parameter gets one lifetime, fn foo<'a>(x: &'a i32); two parameters get two separate lifetimes, fn foo<'a, 'b>(x: &'a i32, y: &'b i32); and so on.
  2. If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32.
  3. If there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters. This makes methods much nicer to read and write, because fewer symbols are necessary.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

90

Chapter 10 · 10.3 Validating References with Lifetimes

Being the compiler: first_word

Let’s pretend we’re the compiler and apply these rules to the signature of first_word in Listing 10.25. The signature starts without any lifetimes associated with the references:

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

The first rule says each parameter gets its own lifetime. We’ll call it 'a as usual:

fn first_word<'a>(s: &'a str) -> &str {

The second rule applies because there is exactly one input lifetime: the lifetime of the one input parameter gets assigned to the output lifetime.

fn first_word<'a>(s: &'a str) -> &'a str {

Now all the references in this signature have lifetimes, and the compiler can continue its analysis without the programmer annotating them.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

91

Chapter 10 · 10.3 Validating References with Lifetimes

Being the compiler: longest

Now longest, which had no lifetime parameters when we started with it in Listing 10.20:

fn longest(x: &str, y: &str) -> &str {

Apply the first rule: each parameter gets its own lifetime. This time we have two parameters instead of one, so we have two lifetimes:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

The second rule doesn’t apply, because there is more than one input lifetime. The third rule doesn’t apply either, because longest is a function rather than a method, so none of the parameters are self. After all three rules, we still haven’t figured out the return type’s lifetime. That is why compiling Listing 10.20 failed: the compiler worked through the elision rules but still couldn’t figure out all the lifetimes of the references in the signature.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

92

Chapter 10 · 10.3 Validating References with Lifetimes

Lifetimes in method definitions

Because the third rule really only applies in method signatures, we look at lifetimes in that context next, to see why the third rule means we don’t have to annotate lifetimes in method signatures very often.

When we implement methods on a struct with lifetimes, we use the same syntax as that of generic type parameters (Listing 10.11). Where we declare and use the lifetime parameters depends on whether they’re related to the struct fields or to the method parameters and return values.

  • Lifetime names for struct fields always need to be declared after the impl keyword and then used after the struct’s name, because those lifetimes are part of the struct’s type.
  • In method signatures inside the impl block, references might be tied to the lifetime of references in the struct’s fields, or they might be independent.
  • The lifetime elision rules often make lifetime annotations unnecessary in method signatures.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

93

Chapter 10 · 10.3 Validating References with Lifetimes

Methods on ImportantExcerpt

Some examples use the struct ImportantExcerpt from Listing 10.24. First, a method named level whose only parameter is a reference to self and whose return value is an i32, which is not a reference to anything:

impl<'a> ImportantExcerpt<'a> {

    fn level(&self) -> i32 {

        3

    }

}

The lifetime parameter declaration after impl and its use after the type name are required, but because of the first elision rule, we’re not required to annotate the lifetime of the reference to self.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

94

Chapter 10 · 10.3 Validating References with Lifetimes

Where the third elision rule applies

Here is an example where the third lifetime elision rule applies:

impl<'a> ImportantExcerpt<'a> {

    fn announce_and_return_part(&self, announcement: &str) -> &str {

        println!("Attention please: {announcement}");

        self.part

    }

}

  • There are two input lifetimes, so Rust applies the first elision rule and gives both &self and announcement their own lifetimes.
  • Then, because one of the parameters is &self, the return type gets the lifetime of &self, and all lifetimes have been accounted for.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

95

Chapter 10 · 10.3 Validating References with Lifetimes

The static lifetime

One special lifetime is 'static, which denotes that the affected reference can live for the entire duration of the program. All string literals have the 'static lifetime, which we can annotate as follows:

let s: &'static str = "I have a static lifetime.";

The text of this string is stored directly in the program’s binary, which is always available. Therefore, the lifetime of all string literals is 'static.

Remark. You might see suggestions in error messages to use the 'static lifetime. Before specifying 'static for a reference, think about whether the reference actually lives the entire lifetime of your program, and whether you want it to. Most of the time, an error message suggesting 'static results from attempting to create a dangling reference or a mismatch of the available lifetimes. In such cases, the solution is to fix those problems, not to specify the 'static lifetime.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

96

Chapter 10 · 10.3 Validating References with Lifetimes

Type parameters, trait bounds and lifetimes together

Let’s briefly look at the syntax of specifying generic type parameters, trait bounds and lifetimes all in one function!

fn main() {

    let string1 = String::from("abcd");

    let string2 = "xyz";

 

    let result = longest_with_an_announcement(

        string1.as_str(),

        string2,

        "Today is someone's birthday!",

    );

    println!("The longest string is {result}");

}

use std::fmt::Display;

 

fn longest_with_an_announcement<'a, T>(

    x: &'a str,

    y: &'a str,

    ann: T,

) -> &'a str

where

    T: Display,

{

    println!("Announcement! {ann}");

    if x.len() > y.len() { x } else { y }

}

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

97

Chapter 10 · 10.3 Validating References with Lifetimes

Reading longest_with_an_announcement

fn longest_with_an_announcement<'a, T>(

  • This is the longest function from Listing 10.21, which returns the longer of two string slices.
  • It now has an extra parameter named ann of the generic type T, which can be filled in by any type that implements the Display trait, as specified by the where clause.
  • This extra parameter is printed using {}, which is why the Display trait bound is necessary.
  • Because lifetimes are a type of generic, the declarations of the lifetime parameter 'a and the generic type parameter T go in the same list inside the angle brackets after the function name.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

98

Chapter 10 · 10.3 Validating References with Lifetimes

Syntax recap: generics, bounds and lifetimes

Table 10.2: The chapter’s generic syntax at a glance

SyntaxMeaning
fn f<T>(x: &[T]) -> &TGeneric type parameter T
fn notify(item: &impl Summary)Any type implementing Summary
fn notify<T: Summary + Display>(item: &T)Trait bounds, combined with +
where T: Display + Clone,Trait bounds moved after the signature
fn f() -> impl SummaryReturns one type implementing Summary
impl<T: Display> ToString for TBlanket implementation
fn f<'a>(x: &'a str) -> &'a strOutput lives as long as the input
struct S<'a> { part: &'a str }Instance can’t outlive the reference
&'static strCan live for the whole program

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

99

Chapter 10 · Summary

Chapter summary

We covered a lot in this chapter! Now that you know about generic type parameters, traits and trait bounds, and generic lifetime parameters, you’re ready to write code without repetition that works in many different situations:

  • Generic type parameters let you apply the code to different types.
  • Traits and trait bounds ensure that even though the types are generic, they’ll have the behavior the code needs.
  • Lifetime annotations ensure that this flexible code won’t have any dangling references.
  • All of this analysis happens at compile time, which doesn’t affect runtime performance!

Remark. There is much more to learn on these topics: Chapter 18 discusses trait objects, another way to use traits. There are also more complex scenarios involving lifetime annotations, needed only in very advanced cases; for those, read the Rust Reference. But next, Chapter 11 teaches how to write tests in Rust, so you can make sure your code works the way it should.

The Rust Programming Language · Ch.10 Generic Types, Traits, and Lifetimes

100