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!