13  Defining Struct(ure)s

Objective

Create new struct types and destructure them. Additionally, add behavior through derive-macros.

You can define a type as being a collection of other types by using the struct keyword.

struct Person {
    name: String,
    age: i32
}

structs are named using PascalCase, as opposed to fns which are named using snake_case convention. Structs are constructed using a “literal” syntax. Where we write the name of the struct followed by curlys and the fields like so:

let me = Person { name: "Josiah".to_string(), age: 29 };

By default, new structs do not get any special behavior. Meaning they cannot be printed using println!():

struct Person {
    name: String,
    age: i32
}

fn main() {
    let me = Person { name: "Josiah".to_string(), age: 29 };
    println!("{me:?}");
}
error[E0277]: `Person` doesn't implement `Debug`
 --> src/main.rs:8:15
  |
8 |     println!("{me:?}");
  |               ^^^^^^ `Person` cannot be formatted using `{:?}`
  |
  = help: the trait `Debug` is not implemented for `Person`
  = note: add `#[derive(Debug)]` to `Person` or manually `impl Debug for Person`
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Person` with `#[derive(Debug)]`
  |
1 + #[derive(Debug)]
2 | struct Person {

Deriving behavior

Rust contains a special type called a trait. They’re akin to S3 generics. They are a a collection of methods that can be implemented by foreign types.

In this case, Debug is a trait that allows for debug printing using println!(). Our new type does not implement this by default. However, we can derive this behavior because everything inside of the struct does.

To derive behavior we use the #[derive()] attribute.

#[derive(Debug, Clone)]
struct Person {
    name: String,
    age: i32
}

The Debug trait allows debug printing of the type Person via dbg!() or println!("{p:?}).

fn main() {
    let me = Person { name: "Josiah".to_string(), age: 29 };
    println!("{me:?}");
}
Person { name: "Josiah", age: 29 }

Accessing Fields

Fields of a struct can be accessed directly, or by reference. Field can be accessed using var.field_name.

If a field is accessed and moved, the struct cannot be moved. The same rules of borrowing apply to a struct. You can borrow a field as much as you want but you can only move it once. Otherwise, the entire struct cannot be used.

fn main() {
    let me = Person { name: "Josiah".to_string(), age: 29 };
    let name = me.name;
    println!("{me:?}");
}
error[E0382]: borrow of partially moved value: `me`
  --> src/main.rs:10:15
   |
9  |     let name = me.name;
   |                ------- value partially moved here
10 |     println!("{me:?}");
   |               ^^^^^^ value borrowed here after partial move

Destructuring

It is possible to destructure a struct during assignment creating many variables at one time.

To destructure during assignment you use the syntax let StructName { field, field } = variable;

fn main() {
    let me = Person { name: "Josiah".to_string(), age: 29 };
    let Person { name, age } = me;
    println!("{name} is {age} years old");
}

Exercise

  • Define a struct called Point which has two fields x, and y that are f64
  • Derive Debug and Clone
  • In main() create a new Point struct
  • Debug print the new point
  • Destructure the point into x and y
View solution
#[derive(Debug, Clone)]
struct Point {
    x: f64,
    y: f64
}

fn main() {
    let point = Point { x: 3.0, y: 0.14 };
    println!("The point is {:?}");
    let Point { x, y } = point;
}