--- title: "Rust Basics - Module 4: Structs, Enums and Pattern Matching" description: "Learn Rust structs, enums, Option, and pattern matching. Understand algebraic data types and how match eliminates null pointer bugs at compile time." author: "Samba Siva" published: 2026-05-10T03:34:20.795Z updated: 2026-11-05T22:19:13.639Z tags: - "Technical" - "Rust" url: "https://sambasivareddy.in/blog/rust-basics-4-structs-enums-and-pattern-matching" --- # Rust Basics - Module 4: Structs, Enums and Pattern Matching In the previous blog post, we discussed about **"Borrowing and References"** and how they make ownership practical. Without borrowing, every function call would consume its arguments, and that makes ownership extremely awkward to work with. In this blog post, we will cover Structs, Enums and Pattern Matching in the Rust. Structs are the **custom data types** which allow us to group related data types into one. And Enums are **algebraic data types** i.e., each variant can carry its own data of different types unlike 'C' enums - a **user-defined data type** used to assign names to integer constants. Let's start with structs, then move to enums and bring it all together with pattern matching. * * * ## Part 1: Structs A struct or structure, is a **custom data type that lets us group and name multiple related values that make up a meaningful group** under a **named type**. ### Definition Using `struct` keyword we can define our own structure in Rust. For example: ```rust struct User { username: String, // Calls as Field email: String, age: u32, active: bool, } ``` ### Creating Instance and Access To use a struct after we defined it, we have to create an instance by giving a concrete value to the each field in the struct. We can do that by stating the name of the struct and then add the curly brackets containing the `key:value` pairs, where the keys are the names of the fields and values are the data we wanted to store. ```rust fn main() { let user = User { username: String::from("siva"), email: String::from("siva@example.com"), age: 25, active: true, }; println!("{}", user.username); } ``` > **Note**: Since `struct` acts as a template for a group, we don't need to create/assign the fields in the same order as we defined in the struct. Now, if we want to access a value from the instance we just created, we can use the `.` notation to access the particular filed and get the value. ```rust let username = user.username; ``` Structs are **immutable** by default - the same rule as any variable. To mutate the fields, the entire binding must be `mut` , as Rust does not allow marking each individual fields as `mut` . **It is all or nothing.** ```rust let mut user = User { username: String::from("siva"), email: String::from("siva@example.com"), age: 25, active: true, }; user.age = 24; // Allowed now ``` And also, we can create a new instance reusing fields from an existing one, or what we call **Struct update syntax.** ```rust let user2 = User { email: String::from("other@example.com"), ..user // fills remaining fields from user defined above } ``` > **Note**: `..user` moves fields that are not `Copy` . If username is a `String` , it moves out of `user` and no long fully usable, so use when it is absolute necessary. * * * ## Part 2: Types of Struct In Rust, we can define or use a struct in three ways, say: 1. Named field struct 2. Tuple struct 3. Unit struct ### Named Field Struct Standard way of defining the structs as we discussed above. ``` struct User { username: String, email: String, age: u32, active: bool, } ``` ### Tuple Struct Tuple structs is **named tuples**, useful when the **field's position** carries meaning: ```rust struct Point(f64, f64); struct Color(u8, u8, u8); let p = Point(3.0, 4.0); let c = Color(255, 128, 0); println!("{}", p.0); // Access fields by position number ``` ### Unit Structs Unit-like structs can be useful when you need to implement a `trait` (we'll discuss more in upcoming modules) on some type but don't have any data that you want to store in the type itself. ```rust struct AlwaysEqual; fn main() { let subject = AlwaysEqual; } ``` * * * ## Part 3: Behaviour with impl - Methods & Associated Functions Methods are similar to functions. We declare them with the `fn` keyword followed by a name, parameters and a return value. Unlike functions, methods are defined within the context of a struct (or enum/trait), and their first parameter is always `self`, which represents the instance of the struct the method is being called on. The only exception for `self` is, if the function is an **associated function to struct** like the function create/initialize a struct. Rust separates data definition from behaviour. We can define the set of methods in an `impl` block, not in the struct itself. ### Example ```rust // Struct (data definition) struct Rectangle { width: f64, height: f64, } // Methods (behaviour) impl Rectangle { // associated fn new(width: f64, height: f64) -> Rectangle { Rectangle { width, height } // field init shortand when name matches } // method - takes &self, borrows the instance fn area(&self) -> f64 { self.width * self.height } // mutable method - takes &mut self fn scale(&mut self, factor: f64) { self.width *= factor; self.height *= factor; } // consuming method - takes self, moves the instance fn into_square(self) -> Rectangle { let side = self.width.min(self.height); Rectangle { width: side, height: side } } } fn main() { let mut r = Rectangle::new(10.0, 5.0); println!("area: {}", r.area()); r.scale(2.0); println!("after scale: {}x{}", r.width, r.height); } ``` The three method receives types - `&self` , `&mut self` , `self` - map directly to the borrowing rules. `&self` borrows immutably. `&mut self` borrows mutably. `self` takes ownership - the caller loses the instance after calling this method. `Rectangle::new` is an associated function (no `self` ). It is the Rust convention for constructors. There is no new keyword - it is just a naming convention. * * * ## Part 4: Enums Also called Enumerations. Enums allow you to define a type by enumerating its possible variants. And this is where Rust diverges sharply from C. Rust enums are algebraic data types - each variant can carry its own data of different types. ### Defining the Enums Using `enum` keyword, we can define an enum in the Rust. For example: ```rust enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64), } ``` This is a single type `Shape` that can be any one of three variants, each carrying different data. Compare this to C where we would need a `union` plus a tag field plus discipline to not access the wrong union member. **_Rust eliminates that entire pattern: the compiler knows which variant is active, and pattern matching forces you to handle each one explicitly._** Another example: ```rust enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } ``` This enum has four variants with different types: - `Quit` : Has no data associated with it at all - `Move` : Has named fields, like a `struct` does - `Write` : Includes a single `String` - `ChangeColor` : Includes three `i32` values. ### Enum Values We can create instances of each of the three variants of `Shape` like this: ```rust let c = Shape::Circle(10.0); let r = Shape::Rectangle(10.0, 15.0); let t = Shape::Triangle(5.0, 6.0, 10.0); ``` ### Standard Rust's Enums The most important enums in all of Rust are built into the standard library: 1. `Option` 2. `Result` `Option` Represents a value that may or may not exists. Rust has no `null` . Instead: ```rust enum Option { Some(T), // there is a value None, // there is no value } let maybe: Option = Some(42); let nothing: Option = None; ``` You cannot use an `Option` as if it were a `T` directly - the compiler forces you to handle the `None` case. **This eliminates an entire class of null pointer bugs.** `Result` Represents either success or failure. ```rust enum Result { Ok(T), // Success, carries the value Err(E), // Failure, carries the error } fn divide(a: f64, b: f64) -> Result { if b == 0.0 { Err(String::from("division by zero")) } else { Ok(a/b) } } ``` * * * ## Part 5: Pattern Matching - `match` `match` is the primary way to work with enums. It is exhaustive - the compiler forces you to handle every variant. No case can be silently ignored. An example using `Shape` enum defined above: ```rust fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(r) => std::f64::consts::PI * r * r, Shape::Rectangle(w, h) => w * h, Shape::Triangle(a, b, c) => { let s = (a + b + c) / 2.0; (s * (s-a) * (s-b) * (s-c)).sqrt() } } } ``` Each arm is `pattern -> expression` . The pattern de-structure the variant and bind its inner values to names. If you miss a variant, the compiler tells you - no runtime surprises. For `Option` : ```rust let value: Option = Some(42); match value { Some(n) => println!("got: {}", n), None => println!("nothing"), }; ``` **Catch-all patterns** ```rust match number { 1 => println!("one"), 2 => println!("two"), _ => println!("something else"), // _ matches anything, binds nothing } ``` `if let` - shorthand when you only care about one variant: ```rust if let Some(n) = value { println!("got: {}", n); } // equivalent to matching Some and ignoring None ``` `while let` - loop while a pattern matches: ```rust while let Some(top) = stack.pop() { println!("{}", top); } ``` * * * ## Part 6: Exercises 1. Define a struct `Transaction` with fields: `id: u64` , `amount: f64` , `status: String` . Add an `impl` block with a constructor `new` and a method `is_large(&self) -> bool` that returns true if `amount > 1000.0` . ```rust struct Transaction { id: u64, amount: f64, status: String, } impl Transaction { fn new(id: u64, amount: f64, status: String) -> Transaction { Transaction { id, amount, status } } fn is_large(&self) -> bool { self.amount > 1000.0 } } fn main() { let txn1 = Transaction::new(1, 900.0, String::from("success")); let txn2 = Transaction::new(2, 1001.0, String::from("success")); println!("Txn1 is large = {}", txn1.is_large()); println!("Txn2 is large = {}", txn2.is_large()); } ``` 1. Define an enum `Command` with variants: `Quit` , `Move { x: i32, y: i32 }` , `Print(String)` . Write a `match` that handles all three and prints something meaningful for each. ```rust enum Command { Quit, Move { x: i32, y: i32 }, Print(String) } fn execute_command(cmd: &Command) { match cmd { Command::Quit => println!("Quitting"), Command::Move{x, y} => println!("Moving x:{} and y:{}", x, y), Command::Print(s) => println!("{}", s) } } fn main() { let cmd1 = Command::Quit; let cmd2 = Command::Move { x: 10, y: 10, }; let cmd3 = Command::Print("Hello".to_string()); execute_command(&cmd1); execute_command(&cmd2); execute_command(&cmd3); } ``` * * * ## Conclusion Structs let you group related data into meaningful types. Enums let a single type represent multiple distinct variants, each carrying its own payload. `match` ties it all together - exhaustive, compiler-enforced, no silent ignores. Together, they replace the scattered nulls, union hacks, and runtime surprises you'd find in C. This is Rust making correctness a compile-time guarantee, not a runtime hope. **What's Next: Traits & Generics** So far, every type we've written is fixed to one kind of data. Traits and Generics change that letting you write code that works across many types while keeping the compiler's full safety guarantees. That's the next module.