Structs are the most common tool to represent a domain by grouping multiple pieces of data into a single type. Rust structs come in three forms — named structs with named fields, tuple structs with only positions, and unit structs without fields. Each has its suitable use case, and you can attach methods using the impl block to use them in an object-oriented manner.
Learning Objectives
- Define named/tuple/unit structs and create instances.
- Define and use methods and associated functions in the
implblock. - Understand the difference between
Selfand&self/&mut self/self. - Understand the meaning of frequently used derive macros such as
#[derive(Debug)].
Key Concepts
1) Named-field struct
struct Rectangle {
width: u32,
height: u32,
}
let r = Rectangle { width: 30, height: 20 };
println!("{}", r.width);
Since it has field names, its intention is clear. It is used most frequently.
2) Tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
red.0 == 255;
Access is by position without field names. Even if the shapes of types are the same, if the names are different, they are not compatible, making it useful for the newtype pattern.
3) Unit struct
struct AlwaysReady;
A type with no fields. Used as a marker for trait implementation, as a "state" in a state machine, or for empty handles.
4) impl block and methods
impl Rectangle {
fn new(w: u32, h: u32) -> Self { Self { width: w, height: h } }
fn area(&self) -> u32 { self.width * self.height }
}
Selfis an alias referring to the type being implemented (Rectangle).- If the first argument is of the
selfseries, method; if not, associated function (such as a constructor). &selfis borrowing,&mut selfis mutable borrowing,selfis ownership-taking method.
5) derive Macro
Frequently used traits can be automatically implemented with macros. The most common one at the beginner level is Debug.
#[derive(Debug)]
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Point { x: 1, y: 2 }
Examples
Example 1 — ex01_named.rs : Rectangle and Methods
//Example 1 of Chapter 11: Named-field structures and methods (impl)
//
//A structure is the most common tool to express a domain concept as a type.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
//Associated function — Constructors like Self::new
fn new(w: u32, h: u32) -> Self {
Self { width: w, height: h }
}
//Method — The first argument is &self
fn area(&self) -> u32 {
self.width * self.height
}
//Method to compare with another rectangle
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let r1 = Rectangle::new(30, 20);
let r2 = Rectangle { width: 10, height: 5 }; //Direct field initialization is also possible
println!("Width of {:?} = {}", r1, r1.area());
println!("Does {:?} contain {:?}? {}", r1, r2, r1.can_hold(&r2));
}
Example 2 — ex02_tuple.rs : Tuple Struct
//Example 11-2: Tuple Struct — Has a name but no field names
#[derive(Debug)]
struct Color(u8, u8, u8);
#[derive(Debug)]
struct Point(f64, f64);
impl Color {
fn brightness(&self) -> u16 {
//Adding u8 risks overflow → Expand to u16
self.0 as u16 + self.1 as u16 + self.2 as u16
}
}
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0);
println!("red = {:?}, brightness = {}", red, red.brightness());
println!("origin = ({}, {})", origin.0, origin.1);
//Tuple structs are not compatible with each other if their types are different
// let x: Color = origin; // ❌
}
Example 3 — ex03_unit.rs : Unit Struct and State Type
//Example 3 of Chapter 11: Unit Struct — Type with No Fields
//
//Often used for markers for trait implementations, state machine indicators, etc.
struct AlwaysReady; //No fields
impl AlwaysReady {
fn ping(&self) -> &'static str {
"pong"
}
}
//A pattern that expresses "state" itself as a type
struct Connected;
struct Disconnected;
fn describe<T>(_state: &T) -> &'static str
where
T: HasName,
{
T::name()
}
trait HasName {
fn name() -> &'static str;
}
impl HasName for Connected {
fn name() -> &'static str { "Connected" }
}
impl HasName for Disconnected {
fn name() -> &'static str { "Disconnected" }
}
fn main() {
let svc = AlwaysReady;
println!("svc.ping() = {}", svc.ping());
let on = Connected;
let off = Disconnected;
println!("on → {}", describe(&on));
println!("off → {}", describe(&off));
}
Common Mistakes
Q. I'm confused between Self and self inside impl.
A. Uppercase Self is the name of the type, lowercase self is the name of the instance. Self { x: 0 } means "create a new instance of this type", self.x refers to "the x of the current instance".
Q. Can I use impl multiple times in the same struct?
A. Yes. It's often used when writing code in separated modules. The compiler collects all of them and treats them as a single definition.
Q. {:?} does not compile without #[derive(Debug)].
A. Correct. Implementation of the Debug trait is required. Usually, it's automatically implemented with derive, and manual impl std::fmt::Debug is needed only when customization is required.
Q. I want to update only some fields and use the existing values for the rest.
A. Use struct update syntax ...
let r2 = Rectangle { width: 100, ..r1 };
However, note that if it includes non-Copy fields, r1 might be moved.
Summary
- Three kinds: named-field / tuple / unit structs. Choose according to the situation.
- Attach methods and associated functions with a
implblock. Self(type) ≠self(instance). Use&self/&mut self/selfaccording to intent.- Automatically implement common traits with
#[derive(Debug, Clone, ...)].
Try it yourself
Solve the assignment below. The reference solution is shown below.