Contents

Ch.5 — Using Structs to Structure Related Data

Chapter 5

Using Structs to Structure Related Data


A struct, or structure, is a custom data type that lets you package together and name multiple related values that make up a meaningful group. If you know an object-oriented language, a struct is like an object’s data attributes. The chapter compares and contrasts tuples with structs, to build on what you already know and show when structs are a better way to group data.

It shows how to define and instantiate structs, and how to define associated functions — especially the kind called methods — to specify behavior associated with a struct type. Structs and enums (Chapter 6) are the building blocks for creating new types in your program’s domain, to take full advantage of Rust’s compile-time type checking.

  • 5.1 Defining and Instantiating Structs
  • 5.2 An Example Program Using Structs
  • 5.3 Methods

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

1

Chapter 5 · 5.1 Defining and Instantiating Structs

Structs versus tuples

Structs are similar to tuples (“The Tuple Type”, Chapter 3): both hold multiple related values, and the pieces can be of different types. Unlike with tuples, in a struct you name each piece of data, so it’s clear what the values mean.

Definition 5.1 (Struct). A custom data type that packages together and names multiple related values that make up a meaningful group.

Adding these names makes structs more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.

To define a struct, enter the keyword struct and name the entire struct; the name should describe the significance of the pieces of data being grouped together. Then, inside curly brackets, define the names and types of the pieces of data.

Definition 5.2 (Field). One named, typed piece of data declared inside a struct’s curly brackets.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

2

Chapter 5 · 5.1 Defining and Instantiating Structs

Defining a struct, then making an instance

Listing 5.1 shows a struct that stores information about a user account.

Listing 5.1: A User struct definition — src/main.rs

struct User {

    active: bool,

    username: String,

    email: String,

    sign_in_count: u64,

}

Definition 5.3 (Instance). A value of a struct type, created by specifying concrete values for each of the struct’s fields.

  • Create an instance by stating the struct’s name, then curly brackets containing key: value pairs: the keys are the field names, the values the data to store in those fields.
  • The fields need not appear in the order in which they were declared in the struct.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

3

Chapter 5 · 5.1 Defining and Instantiating Structs

Creating an instance of User

The struct definition is like a general template for the type; instances fill in that template with particular data to create values of the type. For example, we can declare a particular user:

Listing 5.2: Creating an instance of the User struct — src/main.rs

fn main() {

    let user1 = User {

        active: true,

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

        email: String::from("someone@example.com"),

        sign_in_count: 1,

    };

}

To get a specific value from a struct, use dot notation: this user’s email address is user1.email. If the instance is mutable, you can change a value by using dot notation and assigning into a particular field (Listing 5.3, next).

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

4

Chapter 5 · 5.1 Defining and Instantiating Structs

Changing a field of a mutable instance

Listing 5.3 changes the value in the email field of a mutable User instance.

Listing 5.3: Changing the value in the email field of a User instance — src/main.rs

fn main() {

    let mut user1 = User {

        active: true,

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

        email: String::from("someone@example.com"),

        sign_in_count: 1,

    };

 

    user1.email = String::from("anotheremail@example.com");

}

  • The entire instance must be mutable; Rust doesn’t allow marking only certain fields as mutable.
  • As with any expression, a new struct instance can be the last expression in a function body, to implicitly return that new instance.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

5

Chapter 5 · 5.1 Defining and Instantiating Structs

Returning an instance from a function

Listing 5.4 shows a build_user function that returns a User instance with the given email and username. The active field gets the value true, and sign_in_count gets a value of 1.

Listing 5.4: A build_user function that takes an email and username and returns a User instance — src/main.rs

fn build_user(email: String, username: String) -> User {

    User {

        active: true,

        username: username,

        email: email,

        sign_in_count: 1,

    }

}

It makes sense to name the function parameters the same as the struct fields, but repeating the email and username field names and variables is a bit tedious. With more fields, repeating each name would get even more annoying. Luckily, there’s a convenient shorthand!

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

6

Chapter 5 · 5.1 Defining and Instantiating Structs

Using the field init shorthand

Definition 5.4 (Field init shorthand). When a variable or parameter has exactly the same name as a struct field, you may write just email instead of email: email when creating the instance.

Listing 5.5: A build_user function that uses field init shorthand because the username and email parameters have the same name as struct fields — src/main.rs

fn build_user(email: String, username: String) -> User {

    User {

        active: true,

        username,

        email,

        sign_in_count: 1,

    }

}

It behaves exactly like Listing 5.4: the email field gets the value of the email parameter, and because both have the same name, writing email is enough.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

7

Chapter 5 · 5.1 Defining and Instantiating Structs

Creating instances from other instances

It’s often useful to create a new instance of a struct that includes most of the values from another instance of the same type, but changes some of them. You can do this using struct update syntax.

First, the regular way: user2 gets a new email but otherwise the same values as the user1 of Listing 5.2.

Listing 5.6: Creating a new User instance using all but one of the values from user1 — src/main.rs

fn main() {

    // --snip--

 

    let user2 = User {

        active: user1.active,

        username: user1.username,

        email: String::from("another@example.com"),

        sign_in_count: user1.sign_in_count,

    };

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

8

Chapter 5 · 5.1 Defining and Instantiating Structs

Struct update syntax

Definition 5.5 (Struct update syntax). The syntax ..instance at the end of a struct expression: the remaining fields not explicitly set get the same values as the fields in the given instance.

Listing 5.7: Using struct update syntax to set a new email value for a User instance but to use the rest of the values from user1 — src/main.rs

fn main() {

    // --snip--

 

    let user2 = User {

        email: String::from("another@example.com"),

        ..user1

    };

}

Same effect as Listing 5.6 with less code: user2 has a different email but the same username, active and sign_in_count as user1.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

9

Chapter 5 · 5.1 Defining and Instantiating Structs

Struct update syntax moves data

..user1 must come last; before it, you can set as many fields as you want, in any order, regardless of the order of the fields in the struct’s definition.

Struct update syntax uses = like an assignment, because it moves the data, just as in “Variables and Data Interacting with Move” (Chapter 4).

Example 5.6. In Listing 5.7, which fields of user1 can still be used after creating user2?

  • user1 as a whole can no longer be used: the String in its username field was moved into user2.
  • user1.email can still be used, because its value was not moved out of user1.
  • active and sign_in_count are types that implement the Copy trait, so their values are copied (“Stack-Only Data: Copy”, Chapter 4), not moved.

Had we given user2 new String values for both email and username, and thus only used the active and sign_in_count values from user1, then user1 would still be valid after creating user2.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

10

Chapter 5 · 5.1 Defining and Instantiating Structs

Creating different types with tuple structs

Definition 5.7 (Tuple struct). A struct that looks similar to a tuple: it has the added meaning the struct name provides, but its fields have no names, only types.

Tuple structs are useful when you want to give the whole tuple a name and make it a different type from other tuples, and when naming each field as in a regular struct would be verbose or redundant.

To define one, start with the struct keyword and the struct name, followed by the types in the tuple. Here we define and use two tuple structs named Color and Point:

struct Color(i32, i32, i32);

struct Point(i32, i32, i32);

 

fn main() {

    let black = Color(0, 0, 0);

    let origin = Point(0, 0, 0);

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

11

Chapter 5 · 5.1 Defining and Instantiating Structs

Each tuple struct is its own type

The black and origin values are different types, because they’re instances of different tuple structs. Each struct you define is its own type, even though the fields within the struct might have the same types.

  • A function that takes a parameter of type Color cannot take a Point as an argument, even though both types are made up of three i32 values.
  • Otherwise, tuple struct instances are similar to tuples: you can destructure them into their individual pieces, and use a . followed by the index to access an individual value.
  • Unlike tuples, tuple structs require you to name the type of the struct when you destructure them.

Example 5.8. To destructure the values in the origin point into variables named x, y and z, name the type Point in the pattern:

let Point(x, y, z) = origin;

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

12

Chapter 5 · 5.1 Defining and Instantiating Structs

Defining unit-like structs

Definition 5.9 (Unit-like struct). A struct without any fields. It behaves similarly to (), the unit type (“The Tuple Type”, Chapter 3).

Unit-like structs are useful when you need to implement a trait on some type but don’t have any data you want to store in the type itself (traits: Chapter 10). Declaring and instantiating a unit struct named AlwaysEqual:

struct AlwaysEqual;

 

fn main() {

    let subject = AlwaysEqual;

}

  • Define it with the struct keyword, the name, then a semicolon: no curly brackets or parentheses. Create an instance the same way, with just the name.
  • Imagine later implementing behavior so every AlwaysEqual is always equal to every instance of any other type — perhaps for a known result in tests. That behavior needs no data! Chapter 10 shows how.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

13

Chapter 5 · 5.1 Defining and Instantiating Structs

Ownership of struct data

The User struct in Listing 5.1 uses the owned String type rather than the &str string slice type. This is a deliberate choice: we want each instance of this struct to own all of its data, and that data to be valid for as long as the entire struct is valid.

Structs can also store references to data owned by something else, but that requires lifetimes, a Rust feature discussed in Chapter 10.

Definition 5.10 (Lifetimes). A Rust feature that ensures the data referenced by a struct is valid for as long as the struct is.

If you try to store a reference in a struct without specifying lifetimes, as on the next slide, it won’t work: the compiler complains that it needs lifetime specifiers.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

14

Chapter 5 · 5.1 Defining and Instantiating Structs

Storing references without lifetimes fails

This src/main.rs won’t compile: username and email are references (&str) with no lifetimes specified.

struct User {

    active: bool,

    username: &str,

    email: &str,

    sign_in_count: u64,

}

 

fn main() {

    let user1 = User {

        active: true,

        username: "someusername123",

        email: "someone@example.com",

        sign_in_count: 1,

    };

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

15

Chapter 5 · 5.1 Defining and Instantiating Structs

Error E0106: missing lifetime specifier (1/2)

The compiler complains that it needs lifetime specifiers — first for username:

$ cargo run

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

error[E0106]: missing lifetime specifier

 --> src/main.rs:3:15

  |

3 |     username: &str,

  |               ^ expected named lifetime parameter

  |

help: consider introducing a named lifetime parameter

  |

1 ~ struct User<'a> {

2 |     active: bool,

3 ~     username: &'a str,

  |

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

16

Chapter 5 · 5.1 Defining and Instantiating Structs

Error E0106: missing lifetime specifier (2/2)

error[E0106]: missing lifetime specifier

 --> src/main.rs:4:12

  |

4 |     email: &str,

  |            ^ expected named lifetime parameter

  |

help: consider introducing a named lifetime parameter

  |

1 ~ struct User<'a> {

2 |     active: bool,

3 |     username: &str,

4 ~     email: &'a str,

  |

 

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

error: could not compile `structs` (bin "structs") due to 2 previous errors

Chapter 10 fixes these; for now, use owned types like String instead of references like &str.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

17

Chapter 5 · 5.2 An Example Program Using Structs

The rectangles program, with separate variables

To see when structs help, we write a program that calculates a rectangle’s area, starting with single variables and refactoring until we use structs. A new Cargo binary project, rectangles, takes the width and height in pixels and calculates the area:

Listing 5.8: Calculating the area of a rectangle specified by separate width and height variables — src/main.rs

fn main() {

    let width1 = 30;

    let height1 = 50;

 

    println!(

        "The area of the rectangle is {} square pixels.",

        area(width1, height1)

    );

}

 

fn area(width: u32, height: u32) -> u32 {

    width * height

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

18

Chapter 5 · 5.2 An Example Program Using Structs

It works, but the signature hides the meaning

Run the program with cargo run:

$ cargo run

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s

     Running `target/debug/rectangles`

The area of the rectangle is 1500 square pixels.

Calling area with each dimension succeeds, but we can make this code clearer and more readable. The issue is evident in the signature of area:

fn area(width: u32, height: u32) -> u32 {

area is supposed to calculate the area of one rectangle, yet it has two parameters, and nothing in the program says they are related. Grouping width and height together would be more readable and manageable — one way is tuples (“The Tuple Type”, Chapter 3).

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

19

Chapter 5 · 5.2 An Example Program Using Structs

Refactoring with tuples

Listing 5.9: Specifying the width and height of the rectangle with a tuple — src/main.rs

fn main() {

    let rect1 = (30, 50);

 

    println!(

        "The area of the rectangle is {} square pixels.",

        area(rect1)

    );

}

 

fn area(dimensions: (u32, u32)) -> u32 {

    dimensions.0 * dimensions.1

}

In one way, this is better: tuples add a bit of structure, and we now pass just one argument. In another way it is less clear: tuples don’t name their elements, so we index into the parts, making the calculation less obvious.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

20

Chapter 5 · 5.2 An Example Program Using Structs

Why tuples invite errors; labeling with structs

  • Mixing up width and height wouldn’t matter for the area calculation, but it would if we wanted to draw the rectangle on the screen!
  • We would have to keep in mind that width is the tuple index 0 and height is the tuple index 1.
  • That would be even harder for someone else to figure out and keep in mind if they used our code.
  • Because we haven’t conveyed the meaning of our data in our code, it’s now easier to introduce errors.

Refactoring with structs

We use structs to add meaning by labeling the data. We can transform the tuple into a struct with a name for the whole as well as names for the parts, as Listing 5.10 (next slide) shows.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

21

Chapter 5 · 5.2 An Example Program Using Structs

Defining a Rectangle struct

Listing 5.10: Defining a Rectangle struct — src/main.rs

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

 

    println!(

        "The area of the rectangle is {} square pixels.",

        area(&rect1)

    );

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

22

Chapter 5 · 5.2 An Example Program Using Structs

area borrows a Rectangle

Listing 5.10 (continued): Defining a Rectangle struct — src/main.rs

fn area(rectangle: &Rectangle) -> u32 {

    rectangle.width * rectangle.height

}

We’ve defined a struct named Rectangle. Inside the curly brackets we defined the fields width and height, both of type u32. Then, in main, we created a particular instance of Rectangle with a width of 30 and a height of 50.

The area function now has one parameter, rectangle, whose type is an immutable borrow of a Rectangle instance. As in Chapter 4, we want to borrow the struct rather than take ownership of it: this way main retains ownership and can continue using rect1. That is why we use the & both in the function signature and where we call the function.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

23

Chapter 5 · 5.2 An Example Program Using Structs

A win for clarity

  • area accesses the width and height fields of the Rectangle instance. Accessing fields of a borrowed struct instance does not move the field values, which is why you often see borrows of structs.
  • The signature now says exactly what we mean: calculate the area of Rectangle, using its width and height fields.
  • It conveys that width and height are related, and gives the values descriptive names instead of the tuple indexes 0 and 1.

Table 5.1: The three versions of the rectangles program

ListingSignature of areaDrawback / gain
5.8 variablesfn area(width: u32, height: u32) -> u32two unrelated-looking parameters
5.9 tuplefn area(dimensions: (u32, u32)) -> u32one argument, but unnamed parts .0 and .1
5.10 structfn area(rectangle: &Rectangle) -> u32one borrowed value with named fields

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

24

Chapter 5 · 5.2 An Example Program Using Structs

Adding functionality with derived traits

It’d be useful to print an instance of Rectangle while debugging and see the values of all its fields. Listing 5.11 tries the println! macro as in previous chapters. This won’t work, however.

Listing 5.11: Attempting to print a Rectangle instance — src/main.rs

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

 

    println!("rect1 is {rect1}");

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

25

Chapter 5 · 5.2 An Example Program Using Structs

Structs don’t implement Display

Compiling Listing 5.11 gives an error with this core message:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Definition 5.11 (Display). The formatting println! uses by default for curly brackets {}: output intended for direct end user consumption.

  • The primitive types implement Display by default: there’s only one way you’d want to show a 1 or any other primitive type to a user.
  • Structs have more possibilities (commas? curly brackets? all fields?), so Rust doesn’t guess: structs have no provided Display to use with {}.

Reading on, the errors include this helpful note:

help: the trait `std::fmt::Display` is not implemented for `Rectangle`

  --> src/main.rs:1:1

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

26

Chapter 5 · 5.2 An Example Program Using Structs

Asking for Debug output with :?

Let’s try it! The call becomes println!("rect1 is {rect1:?}"); — the specifier :? inside the curly brackets tells println! to use an output format called Debug.

Definition 5.12 (Debug). A trait that lets you print a value in a way useful for developers while debugging.

Compile with this change. Drat! We still get an error:

error[E0277]: `Rectangle` doesn't implement `Debug`

But again, the compiler gives us a helpful note:

   |                        required by this formatting parameter

   |

Rust does include functionality to print debugging information, but we must explicitly opt in: add the outer attribute #[derive(Debug)] just before the struct definition (Listing 5.12).

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

27

Chapter 5 · 5.2 An Example Program Using Structs

Opting in with #[derive(Debug)]

Listing 5.12: Adding the attribute to derive the Debug trait and printing the Rectangle instance using debug formatting — src/main.rs

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

 

    println!("rect1 is {rect1:?}");

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

28

Chapter 5 · 5.2 An Example Program Using Structs

Debug output: {:?} and pretty {:#?}

Now the program runs without errors and prints:

$ cargo run

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s

     Running `target/debug/rectangles`

rect1 is Rectangle { width: 30, height: 50 }

Nice! It’s not the prettiest output, but it shows the values of all the fields for this instance, which would definitely help during debugging.

For larger structs, output that’s a bit easier to read is useful: use {:#?} instead of {:?} in the println! string. Its output for this example is on the next slide.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

29

Chapter 5 · 5.2 An Example Program Using Structs

Pretty Debug output, and the dbg! macro

Using the {:#?} style outputs:

$ cargo run

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s

     Running `target/debug/rectangles`

rect1 is Rectangle {

    width: 30,

    height: 50,

}

Another way to print a value using the Debug format is the dbg! macro.

Definition 5.13 (dbg! macro). A macro that takes ownership of an expression (as opposed to println!, which takes a reference), prints the file and line number where the dbg! call occurs along with the resultant value of that expression, and returns ownership of the value.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

30

Chapter 5 · 5.2 An Example Program Using Structs

Using dbg! on a field value and on the struct

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let scale = 2;

    let rect1 = Rectangle {

        width: dbg!(30 * scale),

        height: 50,

    };

 

    dbg!(&rect1);

}

Here (left) we want to see the value assigned to the width field, as well as the whole struct in rect1.

Remark. Calling dbg! prints to the standard error console stream (stderr), as opposed to println!, which prints to the standard output console stream (stdout). More on both in “Redirecting Errors to Standard Error”, Chapter 12.

  • We put dbg! around 30 * scale; because dbg! returns ownership of the value, width gets the same value as without the call.
  • We don’t want dbg! to take ownership of rect1, so the next call uses a reference, &rect1.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

31

Chapter 5 · 5.2 An Example Program Using Structs

Reading dbg! output

$ cargo run

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s

     Running `target/debug/rectangles`

[src/main.rs:10:16] 30 * scale = 60

[src/main.rs:14:5] &rect1 = Rectangle {

    width: 60,

    height: 50,

}

  • The first bit of output comes from src/main.rs line 10, where we debug the expression 30 * scale; its resultant value is 60 (the Debug formatting for integers prints only their value).
  • The dbg! call on line 14 outputs the value of &rect1, the Rectangle struct, using the pretty Debug formatting of the type.

The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

32

Chapter 5 · 5.2 An Example Program Using Structs

More derivable traits; from function to method

Beyond Debug

  • Besides Debug, Rust provides a number of traits to use with the derive attribute that add useful behavior to custom types. They and their behaviors are listed in Appendix C.
  • Chapter 10 covers implementing these traits with custom behavior, and creating your own traits.
  • There are also many attributes other than derive: see the “Attributes” section of the Rust Reference.

Where the refactoring goes next

Our area function is very specific: it only computes the area of rectangles. It would help to tie this behavior more closely to our Rectangle struct, because it won’t work with any other type. So we continue refactoring by turning the area function into an area method defined on the Rectangle type.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

33

Chapter 5 · 5.3 Methods

Methods versus functions

Definition 5.14 (Method). A function defined within the context of a struct (or an enum or a trait object), whose first parameter is always self, representing the instance of the struct the method is being called on.

Like functions, methods…

  • are declared with the fn keyword and a name;
  • can have parameters and a return value;
  • contain code that runs when the method is called from somewhere else.

Unlike functions, methods…

  • are defined within the context of a struct — or an enum (Chapter 6) or a trait object (Chapter 18);
  • always take self as their first parameter.

Listing 5.13 (next) changes the area function, which has a Rectangle instance as a parameter, into an area method defined on the Rectangle struct.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

34

Chapter 5 · 5.3 Methods

Method syntax: an impl block for Rectangle

Listing 5.13: Defining an area method on the Rectangle struct — src/main.rs

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

impl Rectangle {

    fn area(&self) -> u32 {

        self.width * self.height

    }

}

Definition 5.15 (impl block). An impl (implementation) block for a type: everything within it is associated with that type. To make area a method, we move the function inside the impl Rectangle curly brackets and change the first (here, only) parameter to self, in the signature and everywhere within the body.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

35

Chapter 5 · 5.3 Methods

Calling a method: rect1.area()

Listing 5.13 (continued): Defining an area method on the Rectangle struct — src/main.rs

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

 

    println!(

        "The area of the rectangle is {} square pixels.",

        rect1.area()

    );

}

Where main called the area function and passed rect1 as an argument, we now use method syntax to call the area method on our Rectangle instance. Method syntax goes after an instance: a dot, the method name, parentheses, and any arguments.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

36

Chapter 5 · 5.3 Methods

&self is short for self: &Self

In the signature for area, we use &self instead of rectangle: &Rectangle. The &self is actually short for self: &Self.

Definition 5.16 (Self). Within an impl block, the type Self is an alias for the type that the impl block is for.

  • Methods must have a parameter named self of type Self as their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot.
  • We still need the & in front of the self shorthand to indicate that this method borrows the Self instance, just as in rectangle: &Rectangle.
  • Methods can take ownership of self, borrow self immutably (as here), or borrow self mutably, just as they can any other parameter.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

37

Chapter 5 · 5.3 Methods

Choosing the receiver, and why methods

We chose &self for the same reason we used &Rectangle in the function version: we don’t want to take ownership, and we only want to read the data in the struct, not write to it.

Table 5.2: The three forms of the first parameter

ReceiverWhen to use it
&selfThe method only reads the instance (borrows it immutably).
&mut selfThe method changes the instance it was called on.
selfRare: the method transforms self into something else, and the caller must not use the original afterward.

Besides providing method syntax and not repeating the type of self in every signature, the main reason for methods is organization: everything we can do with an instance of a type sits in one impl block, instead of making future users of our code search for capabilities of Rectangle in various places in our library.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

38

Chapter 5 · 5.3 Methods

A method with the same name as a field

A method may share a field’s name — here a width method on Rectangle (src/main.rs):

impl Rectangle {

    fn width(&self) -> bool {

        self.width > 0

    }

}

 

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

 

    if rect1.width() {

        println!("The rectangle has a nonzero width; it is {}", rect1.width);

    }

}

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

39

Chapter 5 · 5.3 Methods

Field or method? Parentheses decide; getters

  • The width method returns true if the instance’s width field is greater than 0, and false if it is 0: a field can be used within a method of the same name for any purpose.
  • In main, when we follow rect1.width with parentheses, Rust knows we mean the method width. Without parentheses, Rust knows we mean the field width.

Definition 5.17 (Getter). A method with the same name as a field that only returns the value in the field and does nothing else. Rust does not implement getters automatically for struct fields, as some other languages do.

Often, but not always, a method named like a field is a getter. Getters are useful because you can make the field private but the method public, enabling read-only access to that field as part of the type’s public API. Chapter 7 explains public and private, and how to designate a field or method as either.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

40

Chapter 5 · 5.3 Methods

Where’s the -> operator?

C and C++ use two different operators for calling methods:

  • . when calling a method on the object directly;
  • -> when calling the method on a pointer to the object and the pointer must be dereferenced first. If object is a pointer, object->something() is similar to (*object).something().

Rust doesn’t have an equivalent to the -> operator. Instead, it has a feature called automatic referencing and dereferencing; calling methods is one of the few places in Rust with this behavior.

Definition 5.18 (Automatic referencing and dereferencing). When you call a method with object.something(), Rust automatically adds in &, &mut, or * so that object matches the signature of the method.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

41

Chapter 5 · 5.3 Methods

Implicit borrowing for method receivers

Example 5.19. Let p1 and p2 be Point values, where Point has a method fn distance(&self, other: &Point) -> f64. The following two calls are the same:

p1.distance(&p2);

(&p1).distance(&p2);

The first one looks much cleaner. This automatic referencing works because methods have a clear receiver — the type of self. Given the receiver and name of a method, Rust can figure out definitively whether the method is:

  • reading (&self),
  • mutating (&mut self), or
  • consuming (self).

Making borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

42

Chapter 5 · 5.3 Methods

Methods with more parameters: can_hold

Practice: a second method on Rectangle. An instance takes another Rectangle and returns true if the second can fit completely within self (the first), otherwise false. Once can_hold exists, we want to write Listing 5.14.

Listing 5.14: Using the as-yet-unwritten can_hold method — src/main.rs

fn main() {

    let rect1 = Rectangle {

        width: 30,

        height: 50,

    };

    let rect2 = Rectangle {

        width: 10,

        height: 40,

    };

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

43

Chapter 5 · 5.3 Methods

The expected output of can_hold

Listing 5.14 (continued): Using the as-yet-unwritten can_hold method — src/main.rs

    let rect3 = Rectangle {

        width: 60,

        height: 45,

    };

 

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));

    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));

}

Both dimensions of rect2 are smaller than those of rect1, but rect3 is wider than rect1, so the expected output is:

Can rect1 hold rect2? true

Can rect1 hold rect3? false

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

44

Chapter 5 · 5.3 Methods

Designing the can_hold signature

We know we want a method, so it goes within the impl Rectangle block. Reading the calling code tells us the rest:

  • Name: can_hold.
  • Parameter: an immutable borrow of another Rectangle. rect1.can_hold(&rect2) passes in &rect2, an immutable borrow of rect2, an instance of Rectangle.
  • Why immutable: we only need to read rect2 (writing would need a mutable borrow), and we want main to retain ownership of rect2 so it can use it again after calling can_hold.
  • Return value: a Boolean.
  • Implementation: check whether the width and height of self are greater than the width and height of the other Rectangle, respectively.

Listing 5.15 (next) adds can_hold to the impl block from Listing 5.13.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

45

Chapter 5 · 5.3 Methods

Implementing can_hold

Listing 5.15: Implementing the can_hold method on Rectangle that takes another Rectangle instance as a parameter — src/main.rs

impl Rectangle {

    fn area(&self) -> u32 {

        self.width * self.height

    }

 

    fn can_hold(&self, other: &Rectangle) -> bool {

        self.width > other.width && self.height > other.height

    }

}

Running this with the main function of Listing 5.14 gives the desired output.

Methods can take multiple parameters, added to the signature after the self parameter, and those parameters work just like parameters in functions.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

46

Chapter 5 · 5.3 Methods

Associated functions

Definition 5.20 (Associated function). Any function defined within an impl block; it is associated with the type named after the impl. Methods are the associated functions whose first parameter is self.

  • We can define associated functions that don’t have self as their first parameter (and thus are not methods), because they don’t need an instance of the type to work with.
  • We’ve already used one: the String::from function, defined on the String type.
  • Associated functions that aren’t methods are often used for constructors that return a new instance of the struct. These are often called new, but new isn’t a special name and isn’t built into the language.

For example, an associated function named square could take one dimension parameter and use it as both width and height, making it easier to create a square Rectangle than specifying the same value twice (next slide).

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

47

Chapter 5 · 5.3 Methods

A constructor: Rectangle::square

src/main.rs

impl Rectangle {

    fn square(size: u32) -> Self {

        Self {

            width: size,

            height: size,

        }

    }

}

The Self keywords in the return type and in the body of the function are aliases for the type that appears after the impl keyword — in this case, Rectangle.

To call this associated function, use the :: syntax with the struct name, for example let sq = Rectangle::square(3);. The function is namespaced by the struct: the :: syntax is used both for associated functions and for namespaces created by modules (Chapter 7).

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

48

Chapter 5 · 5.3 Methods

Multiple impl blocks

Each struct may have multiple impl blocks: Listing 5.16, with each method in its own block, equals Listing 5.15.

Listing 5.16: Rewriting Listing 5.15 using multiple impl blocks

impl Rectangle {

    fn area(&self) -> u32 {

        self.width * self.height

    }

}

 

impl Rectangle {

    fn can_hold(&self, other: &Rectangle) -> bool {

        self.width > other.width && self.height > other.height

    }

}

There’s no reason to separate these methods here, but it is valid syntax. Chapter 10 (generic types and traits) shows a case where multiple impl blocks are useful.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

49

Chapter 5 · Summary

Chapter summary

Structs let you create custom types that are meaningful for your domain.

  • With structs you keep associated pieces of data connected to each other, and name each piece to make your code clear.
  • In impl blocks you define functions that are associated with your type.
  • Methods are a kind of associated function that let you specify the behavior that instances of your structs have.

Remark. Structs aren’t the only way to create custom types: Chapter 6 turns to Rust’s enum feature, adding another tool to your toolbox.

The Rust Programming Language · Ch.5 Using Structs to Structure Related Data

50