On this page
Structs: custom data types
Structs: defining your own types
Structs are the primary way to create custom data types in Rust. They let you group related fields under a meaningful name, similar to classes in other languages but without inheritance.
Basic definition
// Define a struct
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
// Create an instance
let rect = Rectangle {
width: 100,
height: 50,
};
println!("Width: {}", rect.width);
println!("Height: {}", rect.height);
println!("Area: {}", rect.width * rect.height);
// Mutable instances
let mut rect2 = Rectangle { width: 200, height: 100 };
rect2.width = 300; // Modify a field
println!("New width: {}", rect2.width);
}Important note: In Rust, mutability applies to the entire instance. You cannot have some fields mutable and others immutable — either the whole struct is mutable or nothing is.
Field initialization shorthand
When you have variables with the same name as the struct fields:
fn create_user(name: String, email: String, age: u32) -> User {
User {
name, // Equivalent to name: name
email, // Equivalent to email: email
age, // Equivalent to age: age
active: true, // This one needs the explicit name
}
}
struct User {
name: String,
email: String,
age: u32,
active: bool,
}Struct update syntax
You can create a new struct based on an existing one:
struct Config {
debug: bool,
max_connections: u32,
timeout_ms: u64,
app_name: String,
}
fn main() {
let base_config = Config {
debug: false,
max_connections: 100,
timeout_ms: 5000,
app_name: String::from("MyApp"),
};
// Only change debug, the rest comes from base_config
// Note: app_name is MOVED (it's a String), not copied
let dev_config = Config {
debug: true,
..base_config
};
println!("Debug mode: {}", dev_config.debug);
println!("Max connections: {}", dev_config.max_connections);
// base_config.app_name is no longer valid (it was moved)
}impl blocks: methods and associated functions
Methods are defined in impl (implementation) blocks. The difference between methods and associated functions:
- Methods: First parameter is
self,&self, or&mut self - Associated functions: No
self— like "static methods" or constructors
struct Circle {
radius: f64,
}
impl Circle {
// Associated function (constructor)
fn new(radius: f64) -> Self {
assert!(radius > 0.0, "Radius must be positive");
Circle { radius }
}
fn unit() -> Self {
Circle::new(1.0)
}
// Read-only methods: &self
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
fn is_larger_than(&self, other: &Circle) -> bool {
self.radius > other.radius
}
// Mutating method: &mut self
fn scale(&mut self, factor: f64) {
self.radius *= factor;
}
// Consuming method: takes ownership (self, no &)
fn into_square_side(self) -> f64 {
self.radius * 2.0 // Side of enclosing square
}
}
fn main() {
let mut c1 = Circle::new(5.0);
let c2 = Circle::unit();
println!("Area of c1: {:.4}", c1.area());
println!("Perimeter of c1: {:.4}", c1.perimeter());
println!("Is c1 > c2? {}", c1.is_larger_than(&c2));
c1.scale(2.0);
println!("Radius after scaling: {}", c1.radius);
let side = c1.into_square_side();
println!("Square side: {}", side);
// c1 is no longer valid — it was consumed by into_square_side
}Derive macros
Rust can automatically generate implementations of certain traits with the #[derive(...)] macro:
#[derive(Debug, Clone, PartialEq, PartialOrd)]
struct Temperature {
celsius: f64,
}
impl Temperature {
fn new(celsius: f64) -> Self {
Temperature { celsius }
}
fn to_fahrenheit(&self) -> f64 {
self.celsius * 1.8 + 32.0
}
fn to_kelvin(&self) -> f64 {
self.celsius + 273.15
}
}
fn main() {
let t1 = Temperature::new(100.0);
let t2 = t1.clone(); // Derived Clone
let t3 = Temperature::new(0.0);
println!("{:?}", t1); // Derived Debug: Temperature { celsius: 100.0 }
println!("{:#?}", t1); // Pretty-print Debug
println!("t1 == t2? {}", t1 == t2); // Derived PartialEq
println!("t1 > t3? {}", t1 > t3); // Derived PartialOrd
println!("{}°C = {}°F = {}K", t1.celsius, t1.to_fahrenheit(), t1.to_kelvin());
}Tuple structs and unit structs
Tuple structs: Structs without field names, only positional types:
struct Meters(f64);
struct Kilograms(f64);
struct Color(u8, u8, u8); // RGB
fn main() {
let distance = Meters(42.5);
let weight = Kilograms(70.0);
let red = Color(255, 0, 0);
println!("Distance: {} meters", distance.0);
println!("Weight: {} kg", weight.0);
println!("RGB: ({}, {}, {})", red.0, red.1, red.2);
// The type system prevents mixing units
// let sum = distance.0 + weight.0; // Doesn't make semantic sense
// Even though it works numerically, using distinct types is better
}Unit structs: Structs with no fields. Useful for implementing traits without data:
struct Agent;
struct Marker;
impl Agent {
fn execute(&self) {
println!("Agent executing...");
}
}
fn main() {
let agent = Agent;
agent.execute();
// Unit structs are useful with generics and traits
let _marker = Marker;
}Multiple impl blocks
You can split the implementation across multiple impl blocks. They are equivalent to a single one:
struct Stack<T> {
data: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { data: Vec::new() }
}
fn push(&mut self, value: T) {
self.data.push(value);
}
fn pop(&mut self) -> Option<T> {
self.data.pop()
}
}
impl<T> Stack<T> {
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn len(&self) -> usize {
self.data.len()
}
fn peek(&self) -> Option<&T> {
self.data.last()
}
}
fn main() {
let mut stack: Stack<i32> = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
println!("Size: {}", stack.len());
println!("Peek: {:?}", stack.peek());
while let Some(value) = stack.pop() {
println!("Popped: {value}");
}
println!("Empty? {}", stack.is_empty());
}Structs are fundamental in Rust. They are used together with enums, which we will cover in the next lesson, to model all the data types of your application with maximum expressiveness and safety.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
impl Point {
// Associated function (constructor)
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
// Method: takes &self (read-only)
fn distance_to_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
// Mutable method: takes &mut self
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let mut p = Point::new(3.0, 4.0);
println!("Point: {p:?}");
println!("Distance to origin: {:.2}", p.distance_to_origin());
p.translate(1.0, -1.0);
println!("After translation: {p:?}");
let origin = Point::origin();
println!("Is origin? {}", p == origin);
}
Sign in to track your progress