Structs
Structs in Rust are custom data types that allow you to group related data together and give it a meaningful name. They are similar to classes in other languages but follow Rust's ownership and borrowing rules.
Defining Structs
A struct is defined using the struct
keyword:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
Field names use snake_case by convention, and each field has a name and a type.
Creating Struct Instances
To create an instance of a struct, specify values for each field:
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
println!("User: {}", user1.username);
}
Accessing Struct Fields
Use dot notation to access struct fields:
fn main() {
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
println!("Email: {}", user1.email);
// Modify a field (struct must be mutable)
user1.email = String::from("[email protected]");
println!("New email: {}", user1.email);
}
Struct Update Syntax
Create new instances using values from existing instances:
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Create user2 using some values from user1
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
..user1 // Use remaining fields from user1
};
// Note: user1 can no longer be used as a whole because String fields were moved
// But we can still access user1.active and user1.sign_in_count (Copy types)
println!("User1 is still active: {}", user1.active);
}
Function Parameters and Return Values
Functions that Take Structs
fn print_user(user: &User) {
println!("User {} ({})", user.username, user.email);
}
fn make_user_inactive(user: &mut User) {
user.active = false;
}
fn main() {
let mut user = User {
email: String::from("[email protected]"),
username: String::from("testuser"),
active: true,
sign_in_count: 1,
};
print_user(&user);
make_user_inactive(&mut user);
println!("User is now active: {}", user.active);
}
Functions that Return Structs
fn build_user(email: String, username: String) -> User {
User {
email, // Shorthand: same as email: email
username, // Shorthand: same as username: username
active: true,
sign_in_count: 1,
}
}
fn main() {
let user = build_user(
String::from("[email protected]"),
String::from("testuser")
);
println!("Created user: {}", user.username);
}
Methods
Methods are functions defined within the context of a struct using impl
blocks:
impl User {
// Method that takes &self (immutable borrow)
fn is_active(&self) -> bool {
self.active
}
// Method that takes &mut self (mutable borrow)
fn deactivate(&mut self) {
self.active = false;
}
// Method that takes self (takes ownership)
fn into_email(self) -> String {
self.email
}
// Method with additional parameters
fn update_email(&mut self, new_email: String) {
self.email = new_email;
self.sign_in_count += 1;
}
}
fn main() {
let mut user = User {
email: String::from("[email protected]"),
username: String::from("testuser"),
active: true,
sign_in_count: 1,
};
println!("Is active: {}", user.is_active());
user.update_email(String::from("[email protected]"));
println!("New email: {}", user.email);
user.deactivate();
println!("Is active: {}", user.is_active());
// This consumes the user
let email = user.into_email();
println!("Final email: {}", email);
// user is no longer available here
}
Associated Functions
Associated functions don't take self
as a parameter and are often used as constructors:
impl User {
// Associated function (constructor)
fn new(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
// Another associated function
fn guest_user() -> User {
User {
email: String::from("[email protected]"),
username: String::from("guest"),
active: false,
sign_in_count: 0,
}
}
}
fn main() {
// Call associated functions using ::
let user1 = User::new(
String::from("[email protected]"),
String::from("alice")
);
let guest = User::guest_user();
println!("User: {}, Guest: {}", user1.username, guest.username);
}
Tuple Structs
Structs that look like tuples but have a name:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
// Access fields by index
println!("Red component: {}", black.0);
println!("X coordinate: {}", origin.0);
// Destructuring
let Color(r, g, b) = black;
println!("RGB: ({}, {}, {})", r, g, b);
}
Methods on Tuple Structs
impl Color {
fn new(r: i32, g: i32, b: i32) -> Color {
Color(r, g, b)
}
fn red(&self) -> i32 {
self.0
}
fn green(&self) -> i32 {
self.1
}
fn blue(&self) -> i32 {
self.2
}
fn brightness(&self) -> f64 {
(self.0 as f64 + self.1 as f64 + self.2 as f64) / 3.0
}
}
fn main() {
let purple = Color::new(128, 0, 128);
println!("Purple brightness: {:.1}", purple.brightness());
}
Unit Structs
Structs with no fields, useful for implementing traits:
struct AlwaysEqual;
impl AlwaysEqual {
fn check(&self) -> bool {
true
}
}
fn main() {
let subject = AlwaysEqual;
println!("Always equal: {}", subject.check());
}
Ownership and Structs
Moving Structs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = rect1; // rect1 is moved to rect2
// println!("{:?}", rect1); // Error: rect1 has been moved
println!("{:?}", rect2); // This works
}
Cloning Structs
#[derive(Debug, Clone)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = rect1.clone(); // Explicit clone
println!("rect1: {:?}", rect1); // Both work
println!("rect2: {:?}", rect2);
}
Copy Structs
Structs containing only Copy types can implement Copy:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point1 = Point { x: 5, y: 10 };
let point2 = point1; // point1 is copied, not moved
println!("point1: {:?}", point1); // Both work
println!("point2: {:?}", point2);
}
Struct Patterns and Destructuring
Basic Destructuring
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
// Destructure into variables
let Point { x, y } = p;
println!("x: {}, y: {}", x, y);
// Destructure with different variable names
let Point { x: a, y: b } = p;
println!("a: {}, b: {}", a, b);
}
Pattern Matching with Structs
fn process_point(point: Point) {
match point {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x: 0, y } => println!("On Y axis at {}", y),
Point { x, y: 0 } => println!("On X axis at {}", x),
Point { x, y } => println!("Point at ({}, {})", x, y),
}
}
fn main() {
let points = vec![
Point { x: 0, y: 0 },
Point { x: 3, y: 0 },
Point { x: 0, y: 5 },
Point { x: 2, y: 3 },
];
for point in points {
process_point(point);
}
}
Nested Structs
struct Address {
street: String,
city: String,
state: String,
zip: String,
}
struct Person {
name: String,
age: u32,
address: Address,
}
impl Person {
fn new(name: String, age: u32, address: Address) -> Person {
Person { name, age, address }
}
fn full_address(&self) -> String {
format!("{}, {}, {} {}",
self.address.street,
self.address.city,
self.address.state,
self.address.zip
)
}
}
fn main() {
let address = Address {
street: String::from("123 Main St"),
city: String::from("Anytown"),
state: String::from("CA"),
zip: String::from("12345"),
};
let person = Person::new(
String::from("Alice"),
30,
address
);
println!("{} lives at {}", person.name, person.full_address());
}
Generic Structs
Structs can be generic over types:
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Point<T> {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
// Implementation for specific types
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let integer_point = Point::new(5, 10);
let float_point = Point::new(1.0, 4.0);
println!("Integer point: {:?}", integer_point);
println!("Float point: {:?}", float_point);
println!("Distance: {}", float_point.distance_from_origin());
}
Multiple Generic Types
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Derive Traits
Common traits can be automatically implemented:
#[derive(Debug, Clone, PartialEq, Eq)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 30, height: 50 };
let rect3 = rect1.clone();
println!("rect1: {:?}", rect1);
println!("rect1 == rect2: {}", rect1 == rect2);
println!("rect1 == rect3: {}", rect1 == rect3);
}
Struct Best Practices
1. Use Descriptive Field Names
// Good
struct User {
username: String,
email_address: String,
is_active: bool,
last_login_timestamp: u64,
}
// Avoid
struct User {
name: String,
email: String,
active: bool,
time: u64,
}
2. Prefer Owned Types for Struct Fields
// Good: Owns its data
struct User {
username: String,
email: String,
}
// More complex: Requires lifetime parameters
// struct User<'a> {
// username: &'a str,
// email: &'a str,
// }
3. Implement Constructor Functions
impl Rectangle {
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
4. Group Related Functionality
impl Rectangle {
// Creation
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Properties
fn area(&self) -> u32 {
self.width * self.height
}
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
// Predicates
fn is_square(&self) -> bool {
self.width == self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Structs are fundamental to organizing data in Rust programs. They provide a way to create custom types that are both expressive and efficient, following Rust's principles of memory safety and zero-cost abstractions.