5. Using Structs to Structure Related Data

Topics covered

  • Structs
  • Methods
  • Other associated functions

Introducing Structs

Defining Structs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
  • Structs are similar to tuples, but with named parts
  • Similar to R named lists, key : value pairs.
  • Defines a new type

Instantiating Structs

fn main() {
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    user1.email = String::from("anotheremail@example.com");
    println!("User1's email: {}",user1.email)
}
  • Instantiate (create) by specifying values for each key
  • To get values, use the . notation. compare to R : user1$email
  • To change values, the entire instance must be mutable.

Constructor

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}
  • Struct is returned as it is the last statement
  • We will see shortly that this will be clearer as an associated function

Shorthand

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}
  • If variable name is same as field name:
    • replace var = var with just var
    • field init shorthand

Struct update syntax

fn main() {
    // --snip--

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}
  • Creates a new User from an existing instance user1
  • Note that this moves data!
    • We can no longer use user1 because we moved the username into user2
    • If we had also given a new username then user1 would ok.

Tuple Structs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}
  • Defines distinct types for Color and Point
  • Access elements by destructuring
  • Alternately can use .0 , .1 etc.

Unit Structs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}
  • Useful for cases where you need a type with a singleton value. (Placeholders or markers)
  • More uses will be clearer when we discuss traits.

References in Struct

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,
    };
}

Ownership of Struct Data

  • Examples so far used owned data (e.g. String)
  • Ensures fields are valid as long as the struct is valid.
  • Structs can store references, but this requires explicitly specifying lifetimes to ensure they remain valid (discussed in Chapter 10).

Example program

Calculate the area of a rectangle

First try

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
}
  • Was it width first or height? (Yeah it doesnt matter here but…)
  • We want area of rectangles, not two numbers

Use Tuples

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
}
  • Combined the two into a single object, but …
  • We dont even have argument names to help us now!

Rectangle struct

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

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
  • area function takes a Rectangle - clearer intent
  • Bit more verbose, but less error prone

Printing Rectangles

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}
  • Try this in the rust playground
  • {} requires implementing std::fmt::Display trait.
  • Built in types implement this, but not user types.
  • Get helpful error message! Try {:?} instead!

Debug printing

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
  • Hmm.. “error[E0277]: Rectangle doesn’t implement Debug

  • But:

    = help: the trait `Debug` is not implemented for `Rectangle`
    = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
  • Rust can do this automatically but we have to tell it to explicitly.

Try it?

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
  • Rust’s helpful error messages can take us far!
  • Use {:#?} to ‘pretty print’ the debug info

dbg! Macro

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale), // dbg! captures this intermediate value
        height: 50,
    };
    dbg!(&rect1);
}
  • Takes ownership but then returns the value - print values inside an expression aids in debugging complex expressions.
  • Prints file and line number
  • Prints to stderr rather then stdout

Rust Playground

More traits

  • Appendix C has more derivable traits.
  • More on traits in Chapter 10

Methods

Defining Methods

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle { //implementaiton block
    fn area(&self) -> u32 {
        self.width * self.height
    }
} // end impl block

fn main() {
 ...
}

Rust Playground

  • Methods are like functions but defined within the context of a struct, enum, or trait object using an impl block
  • First parameter is always self.

Method calls

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}
  • Uses the . syntax, automatically passes in self

  • No need for -> as in c++, Rust automatically dereferences as required to make this more ergonomic!

    p1.distance(&p2);
    (&p1).distance(&p2); \\same but more verbose

Self

  • &self is shorthand for self : &Self
  • Self is shorthand for the object type. (Rectangle)
  • &self or &mut self, borrowing, is most common
  • using just self and taking ownership is rare.

Associated Functions

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}
  • Associated functions are defined in an impl block without self
  • impl organizes code related to the type in one place
  • These functions can refer to the Self type, and commonly do in ‘constructor’ functions as above.
  • Associated functions are called like this: Rectangle::square(3). This should be familiar! String::new().

Other notes

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
    }
}
  • Methods can have more arguments then just self
  • There can be multiple impl blocks. Not needed here but this is useful for generics and traits in Chapter 10.

Summary

  • Structs let you define custom types

  • Methods and associated functions for your custom type are defined in impl blocks, keeping related code together.

  • Next chapter discusses another custom type: Enums