Define custom types with structs, model variants with enums, and unlock Rust's powerful pattern matching.
A struct is a custom data type that groups together multiple related values. There are three types of structs in Rust.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
fn main() {
let mut user = User {
username: String::from("john_doe"),
email: String::from("[email protected]"),
sign_in_count: 1,
active: true,
};
user.sign_in_count += 1;
println!("User: {} ({})", user.username, user.email);
println!("Sign in count: {}", user.sign_in_count);
}
When variable names match field names, you can use shorthand syntax:
fn create_user(username: String, email: String) -> User {
User {
username, // Same as username: username
email, // Same as email: email
sign_in_count: 1,
active: true,
}
fn main() {
let user = create_user(
String::from("jane_doe"),
String::from("[email protected]"),
);
}
Create a new struct from an existing one, changing only some fields:
fn main() {
let user1 = User {
username: String::from("john"),
email: String::from("[email protected]"),
sign_in_count: 1,
active: true,
};
let user2 = User {
username: String::from("jane"),
..user1 // Use remaining fields from user1
};
}
Structs without named fields:
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!("Color red: {}", black.0);
println!("Point x: {}", origin.0);
}
Structs without any fields:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Use the struct update syntax (..other_struct) to create new instances that share most fields with an existing struct. This keeps code DRY and readable.
Methods are functions defined within the context of a struct (or enum, or trait). They take self as their first parameter.
struct Rectangle {
width: u32,
height: u32,
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
fn set_width(&mut self, width: u32) {
self.width = width;
}
fn main() {
let mut rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 20, height: 40 };
println!("Area: {}", rect1.area());
println!("Can hold: {}", rect1.can_hold(&rect2));
rect1.set_width(35);
}
Associated functions don't take self and are called using the :: syntax:
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
fn main() {
let sq = Rectangle::square(10);
println!("Square area: {}", sq.area());
}
Associated functions (like Rectangle::square) act as constructors. They are called with :: instead of . because they don't operate on an instance.
Enums allow you to define a type with a set of possible variants. Each variant can have different data associated with it.
enum Direction {
North,
South,
East,
West,
fn main() {
let direction = Direction::North;
}
enum IpAddr {
V4(String),
V6(String),
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
fn main() {
let ip1 = IpAddr::V4(String::from("127.0.0.1"));
let ip2 = IpAddr::V6(String::from("::1"));
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);
}
impl Message {
fn call(&self) {
println!("Calling message method");
}
fn main() {
let msg = Message::Write(String::from("hello"));
msg.call();
}
Rust enums are far more powerful than enums in most languages. Each variant can hold different types and amounts of data, making them ideal for modelling state machines and message passing.
The Option enum is one of Rust's most powerful features. It represents an optional value:
enum Option<T> {
Some(T),
None,
}
Rust uses Option to represent values that might not exist. This eliminates the need for null pointers:
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
// Extract the value with match
match some_number {
Some(n) => println!("Number: {}", n),
None => println!("No number"),
}
}
Rust has no null. Instead, Option<T> forces you to explicitly handle the case where a value might be absent. The compiler won't let you use an Option<T> as if it were a T, you must unwrap it first.
The match expression lets you compare a value against multiple patterns and execute code based on which pattern matches:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
fn main() {
println!("Penny value: {}", value_in_cents(Coin::Penny));
}
enum UsState {
Alabama,
California,
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Quarter(state) => {
println!("State quarter from {:?}", state);
25
}
_ => 10, // Catch-all pattern
}
}
A match must be exhaustive, every possible value must be covered. Use _ as a catch-all pattern when you don't need to handle every variant individually.
When you only care about one pattern, if let is more concise than match:
fn main() {
let config_max = Some(3u8);
// Using match (verbose)
match config_max {
Some(max) => println!("Maximum is: {}", max),
_ => {},
}
// Using if let (concise)
if let Some(max) = config_max {
println!("Maximum is: {}", max);
}
}
You can also use else with if let:
fn main() {
let coin = Coin::Penny;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}", state);
} else {
println!("Not a quarter");
}
}
Use if let when you only need to match one pattern. Use match when you need to handle multiple patterns or want the compiler to verify exhaustiveness.