What you'll learn: Rust enums as discriminated unions (tagged unions done right),
matchfor exhaustive pattern matching, and how enums replace C++ class hierarchies and C tagged unions with compiler-enforced safety.
std::variant but with exhaustive pattern matching, no std::get exceptions, and no std::visit boilerplateenum is that of the largest possible type. The individual variants are not related to one another and can have completely different typesenum types are one of the most powerful features of the language — they replace entire class hierarchies in C++ (more on this in the Case Studies)fn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let a = Numbers::Zero;
let b = Numbers::SmallNumber(42);
let c : Numbers = a; // Ok -- the type of a is Numbers
let d : Numbers = b; // Ok -- the type of b is Numbers
}
match is the equivalent of the C "switch" on steroids
match can be used for pattern matching on simple data types, struct, enummatch statement must be exhaustive, i.e., they must cover all possible cases for a given type. The _ can be used a wildcard for the "all else" casematch can yield a value, but all arms (=>) must return a value of the same typefn main() {
let x = 42;
// In this case, the _ covers all numbers except the ones explicitly listed
let is_secret_of_life = match x {
42 => true, // return type is boolean value
_ => false, // return type boolean value
// This won't compile because return type isn't boolean
// _ => 0
};
println!("{is_secret_of_life}");
}
match supports ranges, boolean filters, and if guard statementsfn main() {
let x = 42;
match x {
// Note that the =41 ensures the inclusive range
0..=41 => println!("Less than the secret of life"),
42 => println!("Secret of life"),
_ => println!("More than the secret of life"),
}
let y = 100;
match y {
100 if x == 43 => println!("y is 100% not secret of life"),
100 if x == 42 => println!("y is 100% secret of life"),
_ => (), // Do nothing
}
}
match and enums are often combined together
_ if the value is a don't carematches! macro can be used to match to specific variantfn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let b = Numbers::SmallNumber(42);
match b {
Numbers::Zero => println!("Zero"),
Numbers::SmallNumber(value) => println!("Small number {value}"),
Numbers::BiggerNumber(_) | Numbers::EvenBiggerNumber(_) => println!("Some BiggerNumber or EvenBiggerNumber"),
}
// Boolean test for specific variants
if matches!(b, Numbers::Zero | Numbers::SmallNumber(_)) {
println!("Matched Zero or small number");
}
}
match can also perform matches using destructuring and slicesfn main() {
struct Foo {
x: (u32, bool),
y: u32
}
let f = Foo {x: (42, true), y: 100};
match f {
// Capture the value of x into a variable called tuple
Foo{y: 100, x : tuple} => println!("Matched x: {tuple:?}"),
_ => ()
}
let a = [40, 41, 42];
match a {
// Last element of slice must be 42. @ is used to bind the match
[rest @ .., 42] => println!("{rest:?}"),
// First element of the slice must be 42. @ is used to bind the match
[42, rest @ ..] => println!("{rest:?}"),
_ => (),
}
}
🟢 Starter
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
enum CalcResult {
Ok(u64), // Successful result
Invalid(String), // Error message for invalid operations
}
calculate(op: Operation) -> CalcResult
match op {
Operation::Add(a, b) => { /* your code */ },
Operation::Subtract(a, b) => { /* your code */ },
}
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
enum CalcResult {
Ok(u64),
Invalid(String),
}
fn calculate(op: Operation) -> CalcResult {
match op {
Operation::Add(a, b) => CalcResult::Ok(a + b),
Operation::Subtract(a, b) => {
if a >= b {
CalcResult::Ok(a - b)
} else {
CalcResult::Invalid("Underflow".to_string())
}
}
}
}
fn main() {
match calculate(Operation::Add(10, 20)) {
CalcResult::Ok(result) => println!("10 + 20 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
match calculate(Operation::Subtract(5, 10)) {
CalcResult::Ok(result) => println!("5 - 10 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
}
// Output:
// 10 + 20 = 30
// Error: Underflow
impl can define methods associated for types like struct, enum, etc
self as a parameter. self is conceptually similar to passing a pointer to the struct as the first parameter in C, or this in C++self can be immutable (default: &self), mutable (&mut self), or self (transferring ownership)Self keyword can be used a shortcut to imply the typestruct Point {x: u32, y: u32}
impl Point {
fn new(x: u32, y: u32) -> Self {
Point {x, y}
}
fn increment_x(&mut self) {
self.x += 1;
}
}
fn main() {
let mut p = Point::new(10, 20);
p.increment_x();
}
🟡 Intermediate — requires understanding move vs borrow from method signatures
Point
add() will take another Point and will increment the x and y values in place (hint: use &mut self)transform() will consume an existing Point (hint: use self) and return a new Point by squaring the x and ystruct Point { x: u32, y: u32 }
impl Point {
fn new(x: u32, y: u32) -> Self {
Point { x, y }
}
fn add(&mut self, other: &Point) {
self.x += other.x;
self.y += other.y;
}
fn transform(self) -> Point {
Point { x: self.x * self.x, y: self.y * self.y }
}
}
fn main() {
let mut p1 = Point::new(2, 3);
let p2 = Point::new(10, 20);
p1.add(&p2);
println!("After add: x={}, y={}", p1.x, p1.y); // x=12, y=23
let p3 = p1.transform();
println!("After transform: x={}, y={}", p3.x, p3.y); // x=144, y=529
// p1 is no longer accessible — transform() consumed it
}