Structs & enums

Structs & enums

1. Structs with named fields

1.1 — Definition

struct User {
    name: String,
    email: String,
    age: u32,
    active: bool,
}

Type names in PascalCase, fields in snake_case. A struct groups values that go together: it is passed, returned and stored as a whole.

1.2 — Accessing fields

let mut u = User {
    name: String::from("Alice"),
    email: String::from("[email protected]"),
    age: 30,
    active: true,
};

println!("{}", u.name);
u.age += 1;          // the whole variable must be mut

Mutability applies to the whole variable: you cannot make a single field modifiable. For that you need a method that only modifies that field, or interior mutability (Cell, RefCell, see smart-pointers).

1.3 — Field visibility

pub struct Account {
    pub name: String,    // readable and modifiable outside the module
    balance: i64,        // private: only the module that defines Account can access it
}

impl Account {
    pub fn new(name: String) -> Self {
        Self { name, balance: 0 }
    }
    pub fn balance(&self) -> i64 {
        self.balance
    }
}

What it is for: a private field guarantees an invariant. Here, nobody can set an arbitrary balance: they must go through the methods. Consequence: a single private field prevents building the struct with a literal outside the module; you need a constructor.

1.4 — Struct that holds references

struct Excerpt<'a> {
    text: &'a str,
}

A struct that borrows must declare a lifetime: it cannot live longer than what it borrows. See the lifetimes course. To start with, prefer owned fields (String, Vec).

2. Building a struct

2.1 — The full literal

let u = User {
    name: String::from("Alice"),
    email: String::from("[email protected]"),
    age: 30,
    active: true,
};

All the fields are required, in any order.

2.2 — Shorthand when the variable has the name of the field

fn create(name: String, email: String) -> User {
    // Long form
    User { name: name, email: email, age: 0, active: true }
}

fn create_short(name: String, email: String) -> User {
    // Shorthand
    User { name, email, age: 0, active: true }
}

2.3 — Update from another value

let u1 = User { name: "A".into(), email: "[email protected]".into(), age: 30, active: true };

let u2 = User {
    email: String::from("[email protected]"),
    ..u1                          // the other fields come from u1
};

// println!("{}", u1.name);       // ERROR: name was MOVED to u2
println!("{}", u1.age);           // OK: age is Copy, it was copied

..other moves the non-Copy fields. To keep u1 whole, write ..u1.clone().

2.4 — `new` constructor

impl User {
    pub fn new(name: impl Into<String>, email: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            email: email.into(),
            age: 0,
            active: true,
        }
    }
}

let u = User::new("Alice", "[email protected]");

new is not a keyword, only a convention. You write other constructors with meaningful names: User::guest(), Color::from_hex("#fff").

2.5 — Constructor that can fail

pub struct Email(String);

impl Email {
    pub fn parse(text: &str) -> Result<Self, String> {
        if text.contains('@') {
            Ok(Email(text.to_string()))
        } else {
            Err(format!("invalid address: {text}"))
        }
    }
}

What it is for: once built, an Email is necessarily valid. Any function that receives an Email no longer has to check. This is the "parse, don't validate" principle.

2.6 — `Default`

#[derive(Default, Debug)]
struct Options {
    port: u16,        // 0
    host: String,     // ""
    verbose: bool,    // false
}

let o = Options::default();
let o2 = Options { port: 8080, ..Default::default() };   // only what changes

Write Default by hand when the default values are not the "zeros":

struct Config { port: u16, timeout_s: u64 }

impl Default for Config {
    fn default() -> Self {
        Self { port: 8080, timeout_s: 30 }
    }
}

Deriving Default on a struct of limits gives 0 everywhere, which can block everything (a maximum size of 0). In that case, Default is always written by hand.

2.7 — Builder

pub struct Request { url: String, timeout: u64, retries: u32 }

pub struct RequestBuilder { url: String, timeout: u64, retries: u32 }

impl RequestBuilder {
    pub fn new(url: &str) -> Self {
        Self { url: url.to_string(), timeout: 30, retries: 1 }
    }
    pub fn timeout(mut self, s: u64) -> Self { self.timeout = s; self }
    pub fn retries(mut self, n: u32) -> Self { self.retries = n; self }
    pub fn build(self) -> Request {
        Request { url: self.url, timeout: self.timeout, retries: self.retries }
    }
}

let r = RequestBuilder::new("https://ex.com").timeout(5).retries(3).build();

2.8 — Choosing

SituationConstruction
Few fields, all meaningfulliteral or new
An invariant to guaranteeprivate fields + new or parse returning Result
Many fields, most of them defaultDefault + ..Default::default()
Many options, public API that will evolvebuilder

3. Methods and associated functions

3.1 — The four receivers

struct Rectangle { width: u32, height: u32 }

impl Rectangle {
    // Associated function: no self, called with ::
    fn square(side: u32) -> Self {
        Self { width: side, height: side }
    }

    // &self: reads
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // &mut self: modifies
    fn grow(&mut self, n: u32) {
        self.width += n;
        self.height += n;
    }

    // self: consumes and returns something else
    fn into_tuple(self) -> (u32, u32) {
        (self.width, self.height)
    }
}

fn main() {
    let mut r = Rectangle::square(3);
    let a = r.area();
    r.grow(1);
    let (w, h) = r.into_tuple();
    // r.area();        // ERROR: r was consumed
}
ReceiverThe caller after the callTypical use
none—constructors, utilities
&selfkeeps the value intactgetters, computations
&mut selfkeeps the value, modifiedsetters, adding elements
selfloses the valueconversions (into_*), builders, closing a resource

3.2 — Calling by method or by path

let r = Rectangle::square(2);
let a1 = r.area();               // method syntax
let a2 = Rectangle::area(&r);    // path syntax: the same function

The method syntax automatically takes the reference (&r or &mut r) depending on what the method asks for. The path syntax is used to resolve an ambiguity when two traits have a method with the same name.

3.3 — Several `impl` blocks

impl Rectangle {
    fn width(&self) -> u32 { self.width }
}

impl Rectangle {
    fn height(&self) -> u32 { self.height }
}

Allowed: useful to separate methods by theme, or to put some under #[cfg(feature = "...")].

3.4 — Associated constants

impl Rectangle {
    const MAX_SIDE: u32 = 10_000;

    fn is_valid(&self) -> bool {
        self.width <= Self::MAX_SIDE && self.height <= Self::MAX_SIDE
    }
}

3.5 — `Self`

In an impl block, Self refers to the current type. Self { .. }, -> Self and Self::CONSTANT avoid repeating the name, and stay correct if you rename the type.

3.6 — Getter: by reference or by copy

pub struct Profile { name: String, age: u32 }

impl Profile {
    pub fn name(&self) -> &str { &self.name }   // reference: no copy
    pub fn age(&self) -> u32 { self.age }       // Copy: return the value
    pub fn name_owned(&self) -> String { self.name.clone() }  // only if the caller must own it
}

In Rust, a getter has the name of the field (name(), not get_name()).

4. Tuple structs and newtype

4.1 — Definition

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

let black = Color(0, 0, 0);
let red = black.0;
let Point(x, y) = Point(1, 2);

Color and Point are two different types even if their fields have similar types.

4.2 — The newtype: a type for a meaning

struct UserId(u32);
struct OrderId(u32);

fn cancel(order: OrderId) {}

fn main() {
    let u = UserId(5);
    // cancel(u);          // ERROR: expected OrderId, found UserId
    cancel(OrderId(12));
}

What it is for:

  • preventing the mixing of two values of the same raw type (a user identifier passed in place of an order identifier);
  • attaching a validation (Email(String) built by parse);
  • implementing a foreign trait on a foreign type (orphan rule, see traits-avances);
  • hiding a representation: struct Password(String) without a derived Debug is not printed by accident in logs.

The newtype costs nothing at runtime: UserId takes exactly the space of a u32.

4.3 — Accessing the value of a newtype

struct Meters(f64);

impl Meters {
    fn value(&self) -> f64 { self.0 }
}

impl From<Meters> for f64 {
    fn from(m: Meters) -> f64 { m.0 }
}

let m = Meters(3.0);
let a = m.0;            // direct field (if visible)
let b = m.value();      // method
let c: f64 = m.into();  // conversion

5. Unit structs

struct Marker;

struct TestMode;
struct ProdMode;

trait Mode { const NAME: &'static str; }
impl Mode for TestMode { const NAME: &'static str = "test"; }
impl Mode for ProdMode { const NAME: &'static str = "prod"; }

A struct without fields takes no space. It is used to carry a trait or a type without data: a type marker, the implementation of a behaviour (a struct Argon2Hasher; that implements a hashing trait), the state of a builder checked at compile time.

6. Derived traits

6.1 — What each `derive` allows

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
    major: u32,
    minor: u32,
}
TraitWhat it allowsCondition
Debug{:?}, dbg!, assert_eq!all fields Debug
Clone.clone(): explicit copyall fields Clone
Copyimplicit copy on assignmentClone too, Copy fields (no String, Vec)
PartialEq==, !=PartialEq fields
Eqtotal equality (required by HashMap)no f32/f64
HashHashMap key, HashSet elementwith Eq
PartialOrd<, >, <=, >=
Ord.sort(), .max(), BTreeMap keywith Eq and PartialOrd
Default::default()Default fields

6.2 — How the derived comparison works

#[derive(PartialEq, PartialOrd)]
struct Version { major: u32, minor: u32 }

let a = Version { major: 1, minor: 9 };
let b = Version { major: 2, minor: 0 };
assert!(a < b);     // compares major first, then minor: field order

The derived order follows the declaration order of the fields, and for an enum, the order of the variants.

6.3 — Equality by hand

struct User { id: u32, name: String }

impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id       // two users are equal if they have the same id
    }
}

6.4 — `Display`: the output for the user

Display cannot be derived: it is written by hand.

use std::fmt;

struct Version { major: u32, minor: u32 }

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{}", self.major, self.minor)
    }
}

let v = Version { major: 4, minor: 0 };
println!("{v}");             // 4.0
let text = v.to_string();    // to_string comes with Display

6.5 — `Clone` or `Copy`?

Copy is reserved for small values without a resource (numbers, coordinates, identifiers). As soon as a field allocates memory, only Clone is possible, and the .clone() call stays visible in the code: this is intended, because a copy has a cost.

7. Enums

7.1 — Simple definition

#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
    North,
    South,
    East,
    West,
}

let d = Direction::North;

An enum is a type whose value is exactly one of the variants.

7.2 — Importing the variants

use Direction::*;          // avoid outside a small local block

fn opposite(d: Direction) -> Direction {
    match d {
        North => South,
        South => North,
        East => West,
        West => East,
    }
}

Pitfall: if you misspell a variant in a match with use Direction::*, it becomes a variable that catches everything. The compiler warns, but it is better to write Direction::North or Self::North.

7.3 — Discriminants

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
enum Status {
    Draft = 0,
    Published = 1,
    Archived = 9,
}

let code = Status::Archived as u8;   // 9

The reverse direction (u8 to Status) does not exist automatically: a value like 5 would have no variant. You write it with TryFrom:

impl TryFrom<u8> for Status {
    type Error = String;
    fn try_from(v: u8) -> Result<Self, Self::Error> {
        match v {
            0 => Ok(Status::Draft),
            1 => Ok(Status::Published),
            9 => Ok(Status::Archived),
            _ => Err(format!("unknown status: {v}")),
        }
    }
}

let s = Status::try_from(1);    // Ok(Published)
let s: Result<Status, _> = 5_u8.try_into();   // Err; the u8 suffix says which conversion to use

7.4 — Methods on an enum

impl Direction {
    fn is_vertical(self) -> bool {
        matches!(self, Direction::North | Direction::South)
    }

    const ALL: [Direction; 4] = [Direction::North, Direction::South, Direction::East, Direction::West];
}

for d in Direction::ALL {
    println!("{d:?} {}", d.is_vertical());
}

Rust provides no standard way to iterate over the variants: a constant like ALL is the simple solution (or the strum crate).

8. Enums with data

8.1 — Three forms of variants

enum Message {
    Quit,                              // no data
    Move { x: i32, y: i32 },           // named fields
    Write(String),                     // tuple
    Color(u8, u8, u8),
}

let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("hello"));

Each variant carries its own data. The compiler guarantees that you only read x if the value really is a Move.

8.2 — Size in memory

An enum takes the size of its largest variant, plus enough to know which one is active. A very large variant makes every value of the enum bigger: you then put it in a Box.

enum Event {
    Click(u16, u16),
    Report(Box<[u8; 4096]>),   // without Box, every Click would weigh 4 KB
}

8.3 — Recursive enum

enum Expr {
    Number(f64),
    Sum(Box<Expr>, Box<Expr>),
    Product(Box<Expr>, Box<Expr>),
}

fn evaluate(e: &Expr) -> f64 {
    match e {
        Expr::Number(n) => *n,
        Expr::Sum(a, b) => evaluate(a) + evaluate(b),
        Expr::Product(a, b) => evaluate(a) * evaluate(b),
    }
}

let e = Expr::Sum(Box::new(Expr::Number(2.0)), Box::new(Expr::Number(3.0)));
println!("{}", evaluate(&e)); // 5

Without Box, the size of Expr would be infinite: Box stores the sub-expression elsewhere and only keeps a pointer.

8.4 — Conversion to an enum

enum Value { Integer(i64), Text(String) }

impl From<i64> for Value {
    fn from(n: i64) -> Self { Value::Integer(n) }
}
impl From<&str> for Value {
    fn from(s: &str) -> Self { Value::Text(s.to_string()) }
}

let v: Value = 5.into();
let t: Value = "abc".into();

8.5 — `#[non_exhaustive]`

#[non_exhaustive]
pub enum ApiError {
    NotFound,
    Forbidden,
}

Outside the crate that defines it, a match on ApiError must have a _ arm. What it is for: a library can add a variant later without it being a compatibility break. Inside the crate, the match stays exhaustive.

9. `Option<T>`

9.1 — Definition

enum Option<T> {
    None,
    Some(T),
}

Option replaces null: a value that can be absent has the type Option<T>, and you cannot use the T without handling the None.

9.2 — Extracting the value: all the ways

let x: Option<i32> = Some(5);

// 1. match: all the cases, the most explicit
let v = match x {
    Some(n) => n,
    None => 0,
};

// 2. if let: only one case interests you
if let Some(n) = x {
    println!("{n}");
}

// 3. let else: extract or leave the function
// let Some(n) = x else { return; };

// 4. Fallback value
let v = x.unwrap_or(0);                  // value computed in every case
let v = x.unwrap_or_else(|| compute());  // computed only if None
let v = x.unwrap_or_default();           // Default of the type (0, "", empty vec)

// 5. Propagation with ? (in a function that returns Option)
// let n = x?;

// 6. Panic if None (tests, prototypes, truly impossible cases)
let v = x.unwrap();
let v = x.expect("x is always set after initialisation");

fn compute() -> i32 { 0 }
FormWhen to use it
matchthe two cases do different, long things
if letonly Some triggers an action
let ... elseNone must stop the function
unwrap_or*a fallback value makes sense
?the caller must decide
expectNone would be a bug: the message says why it is impossible
unwraptests and prototypes

9.3 — Transforming without extracting

let name: Option<&str> = Some("alice");

let length = name.map(|n| n.len());                      // Some(5)
let uppercase = name.map(str::to_uppercase);             // Some("ALICE")
let short = name.filter(|n| n.len() < 3);                // None
let id = name.and_then(|n| n.parse::<u32>().ok());       // None: and_then for a step that returns Option
let fallback = name.or(Some("guest"));
let res: Result<&str, &str> = name.ok_or("missing name"); // Option -> Result
let all = name.map_or(0, |n| n.len());                    // transform or fallback value

9.4 — Testing

let x = Some(5);
x.is_some();
x.is_none();
x.is_some_and(|n| n > 3);     // true
x.is_none_or(|n| n > 3);      // true if None or if the condition is true

9.5 — Borrowing the content

let name: Option<String> = Some(String::from("alice"));

let r: Option<&String> = name.as_ref();     // without moving name
let s: Option<&str> = name.as_deref();      // Option<String> -> Option<&str>

let mut taken = Some(3);
let old = taken.take();                     // taken becomes None, old = Some(3)
let mut slot = Some(1);
let previous = slot.replace(2);             // slot = Some(2), previous = Some(1)
let mut cache: Option<Vec<i32>> = None;
let v = cache.get_or_insert_with(Vec::new); // creates if absent, returns &mut
v.push(1);

as_deref is the common way to pass an Option<String> to a function that expects Option<&str>.

9.6 — Combining several `Option` values

let a = Some(2);
let b = Some(3);

// 1. match on the tuple
let sum = match (a, b) {
    (Some(x), Some(y)) => Some(x + y),
    _ => None,
};

// 2. zip
let sum = a.zip(b).map(|(x, y)| x + y);

// 3. ? in a function
fn add(a: Option<i32>, b: Option<i32>) -> Option<i32> {
    Some(a? + b?)
}

10. `Result<T, E>`

10.1 — Definition

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result carries either the value, or the reason for the failure. It is marked #[must_use]: ignoring a Result causes a warning.

10.2 — Extracting and transforming

let r: Result<i32, String> = "42".parse::<i32>().map_err(|e| e.to_string());

let v = match &r {
    Ok(n) => *n,
    Err(e) => { eprintln!("{e}"); 0 }
};

let v = r.clone().unwrap_or(0);
let v = r.clone().unwrap_or_else(|e| { eprintln!("{e}"); 0 });
let double = r.clone().map(|n| n * 2);                  // transforms Ok
let msg = r.clone().map_err(|e| format!("error: {e}")); // transforms Err
let next = r.clone().and_then(|n| if n > 0 { Ok(n) } else { Err("negative".into()) });
let opt: Option<i32> = r.clone().ok();                  // Result -> Option (the error is lost)
let is_ok = r.is_ok();

10.3 — `?`: propagating

fn read_age(text: &str) -> Result<u32, std::num::ParseIntError> {
    let age = text.trim().parse::<u32>()?;   // Err: the function returns the error right away
    Ok(age)
}

? is the equivalent of:

let age = match text.trim().parse::<u32>() {
    Ok(v) => v,
    Err(e) => return Err(e.into()),   // into: converts the error if From exists
};

Errors, their types and their conversion are detailed in gestion-des-erreurs.

10.4 — `Option` or `Result`?

  • Option: the absence is normal and needs no explanation (v.first(), map.get(k)).
  • Result: the failure has a cause that the caller may want to know or display (file not found, invalid input).

11. Reading an enum: all the syntaxes

The same function, which returns the text of a message, written in five ways.

11.1 — `match` with destructuring

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

fn describe(m: &Message) -> String {
    match m {
        Message::Quit => "quit".to_string(),
        Message::Move { x, y } => format!("go to {x},{y}"),
        Message::Write(text) => text.clone(),
    }
}

11.2 — Method on the enum with `Self`

impl Message {
    fn describe(&self) -> String {
        match self {
            Self::Quit => "quit".into(),
            Self::Move { x, y } => format!("go to {x},{y}"),
            Self::Write(t) => t.clone(),
        }
    }
}

11.3 — A single case: `if let` or `let else`

fn text(m: &Message) -> Option<&str> {
    if let Message::Write(t) = m {
        Some(t.as_str())
    } else {
        None
    }
}

fn text_alt(m: &Message) -> Option<&str> {
    let Message::Write(t) = m else { return None };
    Some(t.as_str())
}

11.4 — Testing the variant: `matches!`

fn is_move(m: &Message) -> bool {
    matches!(m, Message::Move { .. })
}

fn is_far_move(m: &Message) -> bool {
    matches!(m, Message::Move { x, .. } if x.abs() > 100)
}

11.5 — Ignored fields, binding and guards

fn summary(m: &Message) -> &'static str {
    match m {
        Message::Move { x: 0, y: 0 } => "in place",
        Message::Move { x: 0, .. } => "vertical",
        Message::Move { .. } => "elsewhere",
        Message::Write(t) if t.is_empty() => "empty message",
        Message::Write(_) => "message",
        Message::Quit => "end",
    }
}

All the available patterns are in the pattern-matching course.

12. Modelling well

12.1 — Booleans that contradict each other

// Bad: four combinations, one of them impossible (paid AND cancelled?)
struct Order {
    paid: bool,
    cancelled: bool,
}

// Good: only one possible value at a time
enum OrderState {
    Pending,
    Paid,
    Cancelled,
}

Make impossible states impossible to write: the compiler then does the checking work.

12.2 — Data that exists in only one state

// Bad: shipped_on only makes sense if shipped == true
struct Parcel {
    shipped: bool,
    shipped_on: Option<String>,
    tracking: Option<String>,
}

// Good: each state carries exactly its data
enum Parcel2 {
    Preparing,
    Shipped { date: String, tracking: String },
    Delivered { date: String },
}

12.3 — Struct, enum or trait?

SituationChoice
Several values present togetherstruct
One value among a known, closed setenum
Open set: other crates must be able to add casestrait (see box-dynamiques)
A state with common data and specific datastruct with an enum field
struct Payment {
    amount_cents: i64,
    date: String,
    method: Method,        // common data + variable part
}

enum Method {
    Card { last_four: String },
    Transfer { iban: String },
    Cash,
}

13. Practical examples

Example 1 — accounts and roles

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Role {
    User,
    Moderator,
    Admin,
}

#[derive(Debug)]
struct Account {
    id: u32,
    name: String,
    role: Role,
}

impl Account {
    fn new(id: u32, name: impl Into<String>) -> Self {
        Self { id, name: name.into(), role: Role::User }
    }

    fn promote(&mut self, role: Role) {
        self.role = role;
    }

    fn can_moderate(&self) -> bool {
        self.role >= Role::Moderator      // possible thanks to PartialOrd: the order of the variants
    }
}

fn main() {
    let mut c = Account::new(1, "Alice");
    println!("{}", c.can_moderate());     // false
    c.promote(Role::Admin);
    println!("{}", c.can_moderate());     // true
    println!("{c:?}");
}

Warning: the >= comparison depends on the declaration order. Adding a variant in the wrong place silently changes the permissions. For permissions, an explicit match is safer:

impl Account {
    fn can_moderate_explicit(&self) -> bool {
        match self.role {
            Role::Admin | Role::Moderator => true,
            Role::User => false,
        }
    }
}

Adding a variant then forces you to decide its permissions here, otherwise the code does not compile.

Example 2 — calculator with `Result`

#[derive(Debug, Clone, Copy)]
enum Operation {
    Addition,
    Subtraction,
    Multiplication,
    Division,
}

impl TryFrom<char> for Operation {
    type Error = String;
    fn try_from(c: char) -> Result<Self, Self::Error> {
        match c {
            '+' => Ok(Self::Addition),
            '-' => Ok(Self::Subtraction),
            '*' => Ok(Self::Multiplication),
            '/' => Ok(Self::Division),
            _ => Err(format!("unknown operator: {c}")),
        }
    }
}

fn calculate(a: f64, b: f64, op: Operation) -> Result<f64, String> {
    match op {
        Operation::Addition => Ok(a + b),
        Operation::Subtraction => Ok(a - b),
        Operation::Multiplication => Ok(a * b),
        Operation::Division if b == 0.0 => Err("division by zero".to_string()),
        Operation::Division => Ok(a / b),
    }
}

fn evaluate(a: f64, c: char, b: f64) -> Result<f64, String> {
    let op = Operation::try_from(c)?;
    calculate(a, b, op)
}

fn main() {
    match evaluate(10.0, '/', 0.0) {
        Ok(v) => println!("{v}"),
        Err(e) => println!("error: {e}"),
    }
}

14. Exercises

Exercise 1 — `Book` struct

Create a Book struct (title, author, pages), a new constructor, an is_long method (more than 300 pages) and a Display output "title, by author".

// Solution:
use std::fmt;

struct Book {
    title: String,
    author: String,
    pages: u32,
}

impl Book {
    fn new(title: impl Into<String>, author: impl Into<String>, pages: u32) -> Self {
        Self { title: title.into(), author: author.into(), pages }
    }
    fn is_long(&self) -> bool {
        self.pages > 300
    }
}

impl fmt::Display for Book {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}, by {}", self.title, self.author)
    }
}

Exercise 2 — traffic light

Create a Light enum (Red, Yellow, Green) with a next method and a duration_s method (30, 5, 25).

// Solution:
#[derive(Debug, Clone, Copy, PartialEq)]
enum Light { Red, Yellow, Green }

impl Light {
    fn next(self) -> Self {
        match self {
            Light::Red => Light::Green,
            Light::Green => Light::Yellow,
            Light::Yellow => Light::Red,
        }
    }
    fn duration_s(self) -> u32 {
        match self {
            Light::Red => 30,
            Light::Yellow => 5,
            Light::Green => 25,
        }
    }
}

Exercise 3 — `Option` in three syntaxes

Write first_initial(name: Option<&str>) -> Option<char> with match, with ?, then with and_then.

// Solution:
fn v1(name: Option<&str>) -> Option<char> {
    match name {
        Some(n) => n.chars().next(),
        None => None,
    }
}

fn v2(name: Option<&str>) -> Option<char> {
    name?.chars().next()
}

fn v3(name: Option<&str>) -> Option<char> {
    name.and_then(|n| n.chars().next())
}

Exercise 4 — validated newtype

Create Percentage(u8) that can only be built with a value from 0 to 100.

// Solution:
pub struct Percentage(u8);

impl TryFrom<u8> for Percentage {
    type Error = String;
    fn try_from(v: u8) -> Result<Self, Self::Error> {
        if v <= 100 {
            Ok(Percentage(v))
        } else {
            Err(format!("{v} is not a percentage"))
        }
    }
}

Outside the module, the private field prevents writing Percentage(150).

15. Cheat sheet

Structs

struct S { a: i32, b: String }      // named fields
struct T(i32, i32);                 // tuple struct
struct U;                           // unit struct
struct Id(u32);                     // newtype

S { a, b }                          // shorthand
S { a: 1, ..other }                 // update (moves the non-Copy fields)
S { a: 1, ..Default::default() }

impl S {
    fn new() -> Self { .. }         // associated function
    fn read(&self) {}
    fn modify(&mut self) {}
    fn consume(self) {}
    const MAX: i32 = 10;
}

Enums

enum E { A, B(i32), C { x: i32 } }
#[repr(u8)] enum F { X = 1, Y = 2 }   // F::X as u8
#[non_exhaustive] pub enum G { .. }   // adding variants without a break
enum Tree { Leaf, Node(Box<Tree>, Box<Tree>) }

Option and Result

opt.unwrap_or(v)   opt.unwrap_or_else(|| v)   opt.unwrap_or_default()
opt.map(f)   opt.and_then(f)   opt.filter(p)   opt.ok_or(e)   opt.zip(other)
opt.as_ref()   opt.as_deref()   opt.take()   opt.is_some_and(p)
res.map(f)   res.map_err(f)   res.and_then(f)   res.ok()   res?

Key points

  • A struct groups what goes together, an enum chooses one value among several.
  • ..other moves the non-Copy fields.
  • A private field + a constructor guarantee an invariant.
  • The newtype tells apart values of the same raw type, at no cost.
  • A derived Default gives zeros: write it by hand when zero makes no sense.
  • Option replaces null, Result replaces exceptions.
  • Make impossible states impossible to write: an enum rather than booleans.