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 usematch
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 theradius
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.