Structs & Enums
Define custom types with structs and enums, implement methods, and use Option.
Defining Structs
Structs let you create custom data types by grouping related values together under a meaningful name. Each piece of data in a struct is called a field, and every field has a name and a type. Rust also supports tuple structs which have unnamed fields, and unit structs which have no fields at all. Structs are similar to classes in object-oriented languages but they only contain data; behavior is added separately through impl blocks.
struct Rectangle {
width: f64,
height: f64,
}
struct Color(u8, u8, u8); // tuple struct
fn main() {
let rect = Rectangle {
width: 30.0,
height: 50.0,
};
println!(
"Rectangle: {}x{}, area = {}",
rect.width,
rect.height,
rect.width * rect.height
);
// Struct update syntax
let rect2 = Rectangle {
width: 10.0,
..rect
};
println!("rect2 area: {}", rect2.width * rect2.height);
let red = Color(255, 0, 0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
}
Implementing Methods
Methods are functions defined within an impl block that are associated with a struct. The first parameter of a method is always self, which represents the instance the method is being called on. Methods that take &self borrow the instance immutably, those that take &mut self borrow it mutably, and those that take self consume the instance. Associated functions that do not take self are like static methods and are often used as constructors.
struct Circle {
radius: f64,
}
impl Circle {
// Associated function (constructor)
fn new(radius: f64) -> Circle {
Circle { radius }
}
// Method that borrows self immutably
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
// Method that borrows self mutably
fn scale(&mut self, factor: f64) {
self.radius *= factor;
}
fn is_larger_than(&self, other: &Circle) -> bool {
self.radius > other.radius
}
}
fn main() {
let mut c1 = Circle::new(5.0);
let c2 = Circle::new(3.0);
println!("Area: {:.2}", c1.area());
println!("Circumference: {:.2}", c1.circumference());
println!("c1 > c2: {}", c1.is_larger_than(&c2));
c1.scale(2.0);
println!("After scaling: area = {:.2}", c1.area());
}
Enums
Enums in Rust are far more powerful than in most other languages because each variant can hold different types and amounts of data. An enum defines a type that can be one of several variants, and you use pattern matching to handle each variant. This makes enums ideal for modeling states, messages, and any data that can take one of several forms. Rust enums combined with pattern matching provide a type-safe alternative to inheritance hierarchies.
enum Shape {
Circle(f64),
Rectangle(f64, 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 describe(&self) -> String {
match self {
Shape::Circle(r) => format!("Circle with radius {}", r),
Shape::Rectangle(w, h) => {
format!("Rectangle {}x{}", w, h)
}
Shape::Triangle { base, height } => {
format!("Triangle base={} height={}", base, height)
}
}
}
}
fn main() {
let shapes: Vec<Shape> = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle { base: 3.0, height: 8.0 },
];
for shape in &shapes {
println!("{}: area = {:.2}", shape.describe(), shape.area());
}
}
The Option Type
Rust does not have null values. Instead it uses the Option enum to represent a value that may or may not be present. Option has two variants: Some which wraps a value, and None which indicates absence. The compiler forces you to handle both cases before you can access the inner value, which eliminates null pointer exceptions entirely. This is one of Rust's most important safety features and it makes missing-value bugs impossible in safe code.
fn find_element(haystack: &[i32], needle: i32) -> Option<usize> {
for (index, &item) in haystack.iter().enumerate() {
if item == needle {
return Some(index);
}
}
None
}
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
match find_element(&numbers, 30) {
Some(index) => println!("Found 30 at index {}", index),
None => println!("30 not found"),
}
match find_element(&numbers, 99) {
Some(index) => println!("Found 99 at index {}", index),
None => println!("99 not found"),
}
// Using unwrap_or for default values
let result = divide(10.0, 3.0).unwrap_or(0.0);
println!("10 / 3 = {:.4}", result);
let result = divide(10.0, 0.0).unwrap_or(0.0);
println!("10 / 0 = {}", result);
// Using if let for concise matching
if let Some(val) = divide(22.0, 7.0) {
println!("22 / 7 = {:.4}", val);
}
}