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
voidin 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
enumrepresents 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 Shapes {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn main() {
let dot = Shapes::Dot;
let square = Shapes::Square(10);
let rectangle = Shapes::Rectangle { width: 10, length: 20 };
}
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:
- From a computer science perspective,
enums are tagged unions. - 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
structcontaining anintand aunion.
Doing a match on an enum
- When an
enumhas variants, you usematchto extract the data - New variables are created from the pattern (e.g.
radius)
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn check_shape(shape: Shape) {
match shape {
Shape::Square(width) => {
println!("It's a square, with the width {}", width);
}
_ => {
println!("Try a square instead");
}
}
}
}
Doing a match on an enum
- There are two variables called
width - The binding of
widthin the pattern on line 10 hides thewidthvariable on line 8
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn check_shape(shape: Shape) {
let width = 10;
match shape {
Shape::Square(width) => {
println!("It's a square, with width {}", width);
}
_ => {
println!("Try a square instead");
}
}
}
}
Note:
- Rust allows the variable shadowing shown above in general
Match guards
Match guards allow further refining of a match
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn check_shape(shape: Shape) {
match shape {
Shape::Square(width) if width > 10 => {
println!("It's a BIG square, with width {}", width);
}
_ => {
println!("Try a big square instead");
}
}
}
}
Combining patterns
- You can use the
|operator to join patterns together
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn test_shape(shape: Shape) {
match shape {
Shape::Rectangle { width, .. } | Shape::Square(width) => {
println!("Shape has a width of {}", width);
}
_ => {
println!("Not a rectangle, nor a square");
}
}
}
}
Shorthand: if let conditionals
- You can use
if letif only one case is of interest. - Still pattern matching
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn test_shape(shape: Shape) {
if let Shape::Square(width) = shape {
println!("Shape is a Square with width {}", width);
}
}
}
if let chains in newer Rust versions
Newer Rust versions (edition 2024) allow if let chaining, for example:
enum Shape {
Circle(i32),
Rectangle(i32, i32),
}
fn test_shape(shape: Shape) {
// Hardcoded here, but could be determined by other logic.
let ignore_rectangle = true;
if !ignore_rectangle && let Shape::Rectangle(length, height) = shape {
println!("Shape is a Rectangle with {length} x {height}");
}
}
Shorthand: let else conditionals
- If you expect it to match, but want to handle the error…
- The
elseblock must diverge
#![allow(unused)]
fn main() {
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn test_shape(shape: Shape) {
let Shape::Square(width) = shape else {
println!("I only like squares");
return;
};
println!("Shape is a square with width {}", width);
}
}
Shorthand: while let conditionals
- Keep looping whilst the pattern still matches
enum Shape {
Dot,
Square(u32),
Rectangle { width: u32, length: u32 }
}
fn main() {
while let Shape::Square(width) = make_shape() {
println!("got square, width {}", width);
}
}
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.