Structs & Enums
Structs and enums are Rust's primary tools for creating custom data types. They allow you to group related data together and create meaningful types for your domain.
Defining Structs
A struct (short for "structure") is a custom data type that groups related values together:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("user@example.com"),
username: String::from("rustacean"),
active: true,
sign_in_count: 1,
};
println!("Username: {}", user1.username);
}
Mutable Structs
To modify a struct, the entire instance must be mutable:
struct User {
username: String,
email: String,
active: bool,
}
fn main() {
let mut user1 = User {
email: String::from("user@example.com"),
username: String::from("rustacean"),
active: true,
};
user1.email = String::from("new@example.com");
println!("New email: {}", user1.email);
}
Struct Update Syntax
Create a new instance from an existing one:
struct User {
username: String,
email: String,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("user@example.com"),
username: String::from("rustacean"),
active: true,
};
// Create user2 with some values from user1
let user2 = User {
email: String::from("another@example.com"),
..user1 // Take remaining fields from user1
};
println!("User2: {}", user2.username);
}
Tuple Structs
Structs that look like tuples, useful for giving meaning to tuples:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!("Red value: {}", black.0);
println!("X coordinate: {}", origin.0);
}
Methods on Structs
Use impl blocks to define methods:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method that borrows self
fn area(&self) -> u32 {
self.width * self.height
}
// Method with additional parameters
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// Associated function (no self) - often used as constructors
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let square = Rectangle::square(25);
println!("Area of rect1: {}", rect1.area());
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Square area: {}", square.area());
}
Defining Enums
Enums allow you to define a type by enumerating its possible variants:
enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Going north!"),
Direction::South => println!("Going south!"),
Direction::East => println!("Going east!"),
Direction::West => println!("Going west!"),
}
}
Enums with Data
Enum variants can hold data of different types:
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields (like struct)
Write(String), // Single value
ChangeColor(i32, i32, i32), // Multiple values
}
fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 0, 0);
process_message(msg3);
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: RGB({}, {}, {})", r, g, b),
}
}
The Option Enum
Rust's Option type handles the absence of a value (no null!):
fn main() {
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// Using match
match some_number {
Some(n) => println!("Got number: {}", n),
None => println!("No number"),
}
// Using if let for simpler cases
if let Some(n) = some_number {
println!("The number is: {}", n);
}
}
Methods on Enums
Enums can have methods too:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
impl Coin {
fn value_in_cents(&self) -> u32 {
match self {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
}
fn main() {
let coin = Coin::Quarter;
println!("Value: {} cents", coin.value_in_cents());
}
Practice Exercise
Try this in the playground:
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person {
name: String::from(name),
age,
}
}
fn greet(&self) {
println!("Hi, I'm {} and I'm {} years old!", self.name, self.age);
}
fn have_birthday(&mut self) {
self.age += 1;
println!("Happy birthday! Now I'm {}!", self.age);
}
}
fn main() {
let mut person = Person::new("Alice", 30);
person.greet();
person.have_birthday();
}
Key Takeaways
- Structs group related data with named fields
- Use
implblocks to add methods to structs - Enums define types with multiple variants
- Enum variants can hold different types of data
Option<T>replaces null withSome(T)orNonematchexhaustively handles all enum variantsif letprovides a simpler syntax for single-variant matching
Structs and enums are foundational to writing idiomatic Rust code!
Next Steps
With structs and enums in your toolkit, you're ready for pattern matching — Rust's powerful way to destructure those types and branch on their variants. match and if let turn the enums you just defined into expressive, exhaustive control flow.
Next lesson
Pattern Matching
Master Rust's powerful pattern matching with match expressions, if let, and destructuring
25 min