Rust - Structs
Overview
Estimated time: 50–70 minutes
Learn how to define and use structs in Rust to create custom data types. Structs allow you to package related data together and define methods to work with that data.
Learning Objectives
- Define and instantiate structs
- Access and modify struct fields
- Implement methods and associated functions
- Use different struct syntax patterns
Prerequisites
Defining Structs
Structs let you create custom data types by grouping related data together:
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("alice123"),
email: String::from("[email protected]"),
age: 25,
active: true,
};
println!("User: {}", user1.username);
println!("Email: {}", user1.email);
println!("Age: {}, Active: {}", user1.age, user1.active);
}
Expected output:
User: alice123
Email: [email protected]
Age: 25, Active: true
Mutable Structs
To modify struct fields, the entire struct instance must be mutable:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 0, y: 0 };
println!("Initial point: ({}, {})", point.x, point.y);
point.x = 10;
point.y = 20;
println!("Updated point: ({}, {})", point.x, point.y);
}
Expected output:
Initial point: (0, 0)
Updated point: (10, 20)
Struct Update Syntax
You can create new instances from existing ones using struct update syntax:
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("alice123"),
email: String::from("[email protected]"),
age: 25,
active: true,
};
// Create user2 based on user1, but with different email
let user2 = User {
email: String::from("[email protected]"),
..user1 // Use remaining fields from user1
};
// Note: user1 is no longer valid because String fields were moved
println!("User2: {}", user2.username);
println!("User2 email: {}", user2.email);
}
Expected output:
User2: alice123
User2 email: [email protected]
Tuple Structs
Tuple structs have fields without names, useful for creating distinct types:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0, 0, 0);
println!("Red color: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin point: ({}, {}, {})", origin.0, origin.1, origin.2);
// Color and Point are different types even though they have same structure
// let mixed = red + origin; // This would be an error
}
Expected output:
Red color: (255, 0, 0)
Origin point: (0, 0, 0)
Unit-Like Structs
Structs without fields, useful for implementing traits on types without data:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
let another = AlwaysEqual;
println!("Created unit-like structs");
// Useful for implementing traits or as markers
}
Expected output:
Created unit-like structs
Methods
Define methods on structs using impl
blocks:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method with &self (immutable borrow)
fn area(&self) -> u32 {
self.width * self.height
}
// Method with &mut self (mutable borrow)
fn double_size(&mut self) {
self.width *= 2;
self.height *= 2;
}
// Method that takes ownership
fn into_square(self) -> Rectangle {
let size = std::cmp::max(self.width, self.height);
Rectangle {
width: size,
height: size,
}
}
// Method with parameters
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let mut rect1 = Rectangle {
width: 30,
height: 50,
};
println!("Area: {}", rect1.area());
rect1.double_size();
println!("After doubling: {}x{}", rect1.width, rect1.height);
let rect2 = Rectangle {
width: 10,
height: 20,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
let square = rect1.into_square();
println!("Square: {}x{}", square.width, square.height);
// rect1 is no longer valid here
}
Expected output:
Area: 1500
After doubling: 60x100
Can rect1 hold rect2? true
Square: 100x100
Associated Functions
Functions associated with a struct but don't take self
as a parameter:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function (constructor)
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Associated function to create a square
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// Method
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
// Call associated functions using ::
let rect1 = Rectangle::new(10, 20);
let square = Rectangle::square(15);
println!("Rectangle area: {}", rect1.area());
println!("Square area: {}", square.area());
}
Expected output:
Rectangle area: 200
Square area: 225
Multiple impl Blocks
You can have multiple impl
blocks for the same struct:
struct Circle {
radius: f64,
}
impl Circle {
fn new(radius: f64) -> Circle {
Circle { radius }
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Circle {
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
fn diameter(&self) -> f64 {
2.0 * self.radius
}
}
fn main() {
let circle = Circle::new(5.0);
println!("Circle with radius: {}", circle.radius);
println!("Area: {:.2}", circle.area());
println!("Circumference: {:.2}", circle.circumference());
println!("Diameter: {:.2}", circle.diameter());
}
Expected output:
Circle with radius: 5
Area: 78.54
Circumference: 31.42
Diameter: 10.00
Practical Example: Bank Account
struct BankAccount {
account_number: String,
holder_name: String,
balance: f64,
}
impl BankAccount {
fn new(account_number: String, holder_name: String) -> BankAccount {
BankAccount {
account_number,
holder_name,
balance: 0.0,
}
}
fn deposit(&mut self, amount: f64) {
if amount > 0.0 {
self.balance += amount;
println!("Deposited ${:.2}. New balance: ${:.2}", amount, self.balance);
} else {
println!("Invalid deposit amount: ${:.2}", amount);
}
}
fn withdraw(&mut self, amount: f64) -> bool {
if amount > 0.0 && amount <= self.balance {
self.balance -= amount;
println!("Withdrew ${:.2}. New balance: ${:.2}", amount, self.balance);
true
} else {
println!("Invalid withdrawal: ${:.2} (Balance: ${:.2})", amount, self.balance);
false
}
}
fn get_balance(&self) -> f64 {
self.balance
}
fn account_info(&self) {
println!("Account: {}", self.account_number);
println!("Holder: {}", self.holder_name);
println!("Balance: ${:.2}", self.balance);
}
}
fn main() {
let mut account = BankAccount::new(
String::from("ACC123456"),
String::from("Alice Johnson")
);
account.account_info();
account.deposit(1000.0);
account.deposit(250.50);
account.withdraw(300.0);
account.withdraw(2000.0); // Should fail
println!("Final balance: ${:.2}", account.get_balance());
}
Expected output:
Account: ACC123456
Holder: Alice Johnson
Balance: $0.00
Deposited $1000.00. New balance: $1000.00
Deposited $250.50. New balance: $1250.50
Withdrew $300.00. New balance: $950.50
Invalid withdrawal: $2000.00 (Balance: $950.50)
Final balance: $950.50
Common Pitfalls
1. Forgetting to Make Struct Mutable
struct Point {
x: i32,
y: i32,
}
fn main() {
// Wrong: immutable struct
// let point = Point { x: 0, y: 0 };
// point.x = 10; // Error!
// Correct: mutable struct
let mut point = Point { x: 0, y: 0 };
point.x = 10; // OK
println!("Point: ({}, {})", point.x, point.y);
}
2. Moving Ownership with Struct Update
struct User {
name: String,
age: u32,
}
fn main() {
let user1 = User {
name: String::from("Alice"),
age: 25,
};
let user2 = User {
age: 30,
..user1 // user1.name is moved here
};
// println!("{}", user1.name); // Error: value borrowed after move
println!("{}", user2.name); // OK
}
3. Method vs Associated Function Syntax
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
// Associated function: use ::
let rect = Rectangle::new(10, 20);
// Method: use .
let area = rect.area();
println!("Area: {}", area);
}
Checks for Understanding
Question 1: What's the difference between a method and an associated function?
Answer: A method takes &self
, &mut self
, or self
as the first parameter and is called with dot notation (e.g., instance.method()
). An associated function doesn't take self
and is called with ::
notation (e.g., Type::function()
).
Question 2: Why might this code not compile?
struct Point { x: i32, y: i32 }
let point = Point { x: 0, y: 0 };
point.x = 10;
Answer: The point
variable is immutable. To modify struct fields, the entire struct instance must be declared as mutable: let mut point = Point { x: 0, y: 0 };
Question 3: How would you create a constructor function for this struct?
struct Person {
name: String,
age: u32,
}
Answer:
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}