Compound Types

Structs

A struct groups and names data of different types.

Definition

#![allow(unused)]
fn main() {
struct Point {
    x: i32,
    y: i32,
}
}

Note:

The fields may not be laid out in memory in the order they are written (unless you ask the compiler to ensure that they are).

Construction

  • there is no partial initialization
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
}

Construction

  • but you can copy from an existing variable of the same type
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    let q = Point { x: 4, ..p };
}

Field Access

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{}", p.x);
    println!("{}", p.y);
}

Tuples

  • Holds values of different types together.
  • Like an anonymous struct, with fields numbered 0, 1, etc.
fn main() {
    let p = (1, 2);
    println!("{}", p.0);
    println!("{}", p.1);
}

()

  • the empty tuple
  • represents the absence of data
  • we often use this similarly to how you’d use void in C
#![allow(unused)]
fn main() {
fn prints_but_returns_nothing(data: &str) -> () {
    println!("passed string: {}", data);
}
}

Tuple Structs

  • Like a struct, with fields numbered 0, 1, etc.
struct Point(i32,i32);

fn main() {
    let p = Point(1, 2);
    println!("{}", p.0);
    println!("{}", p.1);
}

Enums

  • An enum represents different variations of the same subject.
  • The different choices in an enum are called variants

enum: Definition and Construction

enum Shape {
    Square,
    Circle,
    Rectangle,
    Triangle,
}

fn main() {
    let shape = Shape::Rectangle;
}

Enums with Values

enum Movement {
    Right(i32),
    Left(i32),
    Up(i32),
    Down { speed: i32, excitement: u8 },
}

fn main() {
    let movement = Movement::Left(12);
    let movement = Movement::Down { speed: 12, excitement: 5 };
}

Enums with Values

  • An enum value is the same size, no matter which variant is picked
  • It will be the size of the largest variant (plus a tag)

Note:

The tag in an enum specifies which variant is currently valid, and is stored as the smallest integer the compiler can get away with - it depends how many variants you have. Of course, if none of the variants have any data, the enum is just the tag.

If you have a C background, you can think of this as being a struct containing an int and a union.

Doing a match on an enum

  • When an enum has variants, you use match to extract the data
  • New variables are created from the pattern (e.g. radius)
#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn check_shape(shape: Shape) {
    match shape {
        Shape::Circle(radius) => {
            println!("It's a circle, with radius {}", radius);
        }
        _ => {
            println!("Try a circle instead");
        }
    }
}
}

Doing a match on an enum

  • There are two variables called radius
  • The binding of radius in the pattern on line 9 hides the radius variable on line 7
#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn check_shape(shape: Shape) {
    let radius = 10;
    match shape {
        Shape::Circle(radius) => {
            println!("It's a circle, with radius {}", radius);
        }
        _ => {
            println!("Try a circle instead");
        }
    }
}
}

Match guards

Match guards allow further refining of a match

#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn check_shape(shape: Shape) {
    match shape {
        Shape::Circle(radius) if radius > 10 => {
            println!("It's a BIG circle, with radius {}", radius);
        }
        _ => {
            println!("Try a big circle instead");
        }
    }
}
}

Combining patterns

  • You can use the | operator to join patterns together
#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
    Square(i32),
}

fn test_shape(shape: Shape) {
    match shape {
        Shape::Circle(size) | Shape::Square(size) => {
            println!("Shape has single size field {}", size);
        }
        _ => {
            println!("Not a circle, nor a square");
        }
    }
}
}

Shorthand: if let conditionals

  • You can use if let if only one case is of interest.
  • Still pattern matching
#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn test_shape(shape: Shape) {
    if let Shape::Circle(radius) = shape {
        println!("Shape is a Circle with radius {}", radius);
    }
}
}

Shorthand: let else conditionals

  • If you expect it to match, but want to handle the error...
  • The else block must diverge
#![allow(unused)]
fn main() {
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn test_shape(shape: Shape) {
    let Shape::Circle(radius) = shape else {
        println!("I only like circles");
        return;
    };
    println!("Shape is a Circle with radius {}", radius);
}
}

Shorthand: while let conditionals

  • Keep looping whilst the pattern still matches
enum Shape {
    Circle(i32),
    Rectangle(i32, i32),
}

fn main() {
    while let Shape::Circle(radius) = make_shape() {
        println!("got circle, radius {}", radius);
    }
}

fn make_shape() -> Shape {
    todo!()
}

Foreshadowing! 👻

Two very important enums

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}

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

We'll come back to them after we learn about error handling.