Structs and Enums
Scope: grouping data with structs, modeling alternatives with
enums, and meeting Option and Result — the types that replace
null and exceptions. All examples were compiled and run.
Structs: named bundles of data
struct User {
name: String,
age: u32,
active: bool,
}
fn main() {
let ada = User {
name: String::from("Ada"),
age: 36,
active: true,
};
println!("{} is {}", ada.name, ada.age);
}
- A struct defines the shape;
User { ... }builds one. - Every field must get a value, and field order doesn't matter.
- To change a field later, the whole variable must be
mut:
let mut ada = User { name: String::from("Ada"), age: 36, active: true };
ada.age = 37;
Methods: functions attached to a struct
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let r = Rectangle { width: 3, height: 4 };
println!("area: {}", r.area());
}
impl Rectangleopens a block of methods for the struct.&selfmeans "borrow the struct I'm called on" —r.area()borrowsr, soris still usable afterward.- No classes, no inheritance: structs hold data,
implblocks add behavior. Shared behavior across types comes from traits (beyond this guide's scope — the glossary points onward).
Enums: one of several possibilities
enum Direction {
North,
South,
East,
West,
}
let heading = Direction::North;
Variants can carry data:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
match handles every variant — and the compiler forces you to
handle all of them:
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "up",
Direction::South => "down",
Direction::East => "right",
Direction::West => "left",
}
}
Leave out a variant and the compiler errors: "non-exhaustive patterns." You cannot forget a case. This is one of Rust's quiet superpowers.
Option: Rust's answer to null
There is no null in Rust. "Might not have a value" is Option:
enum Option<T> { // (simplified — it's in the standard library)
Some(T),
None,
}
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Ada"))
} else {
None
}
}
fn main() {
match find_user(1) {
Some(name) => println!("found {}", name),
None => println!("no such user"),
}
}
You can't accidentally use a possibly-missing value — the compiler
makes you handle None before touching the Some. An entire
category of "null pointer" crashes becomes impossible.
Shortcuts you'll see (same idea, less typing):
let name = find_user(1).unwrap_or(String::from("guest"));
Result: Rust's answer to exceptions
There are no exceptions either. "Might fail" is Result:
enum Result<T, E> { // (simplified)
Ok(T),
Err(E),
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("can't divide by zero"))
} else {
Ok(a / b)
}
}
The ? operator propagates errors upward — "return early if this
failed":
fn read_number() -> Result<i32, String> {
let text = "42";
let n: i32 = text.parse().map_err(|_| String::from("not a number"))?;
Ok(n * 2)
}
If parse() fails, ? returns the Err immediately. If it
succeeds, execution continues with the unwrapped value. Error
handling is visible in the code, not hidden in a catch block
three functions away.
Check
- [ ] You can build a struct, read its fields, and add a method
- [ ] You can explain why
matchmust cover every enum variant - [ ] You can say what replaces
null(Option) and exceptions (Result) — and why that's safer
Next: Error Handling goes deeper on Result.