Defining Structs
Structs are custom data types that group related fields together. They are similar to classes in other languages but without inheritance. Rust uses composition and traits instead.
// Named-field struct
#[derive(Debug)]
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
fn main() {
// Creating an instance
let user1 = User {
username: String::from("ferris"),
email: String::from("ferris@rust-lang.org"),
active: true,
sign_in_count: 1,
};
println!("{:?}", user1);
// Mutable struct (entire struct must be mutable)
let mut user2 = User {
username: String::from("crab"),
email: String::from("crab@sea.com"),
active: true,
sign_in_count: 0,
};
user2.sign_in_count += 1;
// Struct update syntax (like spread in JS)
let user3 = User {
email: String::from("new@email.com"),
..user2 // remaining fields from user2 (moves String fields!)
};
println!("{:?}", user3);
}
Methods with impl Blocks
#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// Associated function (like a static method) — no self
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
fn square(size: f64) -> Self {
Rectangle { width: size, height: size }
}
// Method — takes &self (immutable borrow)
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
// Method that takes &mut self
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// Method that takes self (consumes the struct)
fn into_description(self) -> String {
format!("{}x{} rectangle", self.width, self.height)
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let mut rect = Rectangle::new(30.0, 50.0);
println!("Area: {}", rect.area());
println!("Perimeter: {}", rect.perimeter());
rect.scale(2.0);
println!("Scaled: {:?}", rect);
let small = Rectangle::square(10.0);
println!("Can hold? {}", rect.can_hold(&small));
let desc = rect.into_description();
println!("{desc}");
// rect is consumed — can't use it anymore
}
Tuple Structs and Unit Structs
// Tuple structs — named tuples
struct Color(u8, u8, u8);
struct Point(f64, f64, f64);
// Even though both have similar fields, they're different types
// let p: Point = Color(255, 0, 0); // ERROR: type mismatch
// Unit struct — zero-sized, useful for traits
struct Marker;
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
}
Enums
Rust enums are algebraic data types — each variant can hold different types and amounts of data. They're far more powerful than enums in C or Java.
#[derive(Debug)]
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
println!("{:?} {:?}", home, loopback);
let shapes = vec![
Shape::Circle { radius: 5.0 },
Shape::Rectangle { width: 4.0, height: 6.0 },
Shape::Triangle { base: 3.0, height: 8.0 },
];
for shape in &shapes {
println!("{:?} area = {:.2}", shape, shape.area());
}
}
Option<T> and Result<T, E>
No Null in Rust
Rust has no null. Instead, it uses Option<T> to represent
a value that might be absent, and Result<T, E> for operations that
might fail. The compiler forces you to handle both cases.
fn main() {
// Option = Some(T) | None
let some_number: Option = Some(42);
let no_number: Option = None;
// You MUST handle the None case
match some_number {
Some(n) => println!("Got: {n}"),
None => println!("Nothing"),
}
// Useful Option methods
let val = some_number.unwrap_or(0); // 42
let val2 = no_number.unwrap_or_default(); // 0
let mapped = some_number.map(|n| n * 2); // Some(84)
let filtered = some_number.filter(|&n| n > 100); // None
println!("{val} {val2} {mapped:?} {filtered:?}");
// Result = Ok(T) | Err(E)
let result: Result = Ok(42);
let error: Result = Err("oops".to_string());
match result {
Ok(n) => println!("Success: {n}"),
Err(e) => println!("Error: {e}"),
}
// Practical example
let parsed: Result = "42".parse();
let failed: Result = "abc".parse();
println!("{:?} {:?}", parsed, failed);
}
Key Takeaways
- ✅ Structs group data; impl blocks add methods and associated functions
- ✅ Methods take
&self,&mut self, orselfdepending on access needs - ✅ Enums can hold data in each variant — they're algebraic data types
- ✅
Option<T>replaces null;Result<T, E>replaces exceptions - ✅ The compiler forces you to handle all cases, eliminating null pointer errors