Site

Structs — Rust Data Grouping

Tutorial 7.0  •  Rust / Learn

7.0 What This Teaches

Structs are Rust's primary tool for grouping related data under a named type. This tutorial covers:

7.1 Defining a Struct

A struct declaration names the type and lists its fields, each with a name and type. By convention, struct names use UpperCamelCase and fields use snake_case.
struct Point {
    x: f64,
    y: f64,
}
To create an instance, supply a value for every field:
let p = Point { x: 3.0, y: 4.0 };
println!("x = {}, y = {}", p.x, p.y);
Fields are accessed with .. Instances are immutable by default; add mut to the binding to allow field mutation:
let mut p = Point { x: 0.0, y: 0.0 };
p.x = 5.0;
Rust does not allow marking individual fields mut - mutability applies to the whole instance through its binding.

7.2 impl Blocks

An impl block attaches functions to a struct. There are two kinds:
impl Point {
    fn new(x: f64, y: f64) -> Point {
        Point { x, y }   // field init shorthand: x means x: x
    }

    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    fn translate(&self, dx: f64, dy: f64) -> Point {
        Point { x: self.x + dx, y: self.y + dy }
    }
}
Point::new(3.0, 4.0) calls the associated function. p.distance_from_origin() calls a method - Rust automatically passes &p as self.

7.3 Field Init Shorthand

When a variable name matches the field name, you can omit the repetition:
fn new(x: f64, y: f64) -> Point {
    Point { x, y }   // same as Point { x: x, y: y }
}
This is purely a convenience - the generated code is identical.

7.4 Derived Traits

Rust can automatically implement common traits for a struct by adding a #[derive] attribute. The three most useful for beginners are:
TraitWhat it provides
Debug{:?} and {:#?} formatting for println!
Clone.clone() method for explicit deep copy
PartialEq== and != comparison between instances
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
With Debug derived:
let p = Point::new(3.0, 4.0);
println!("{:?}",  p);   // Point { x: 3.0, y: 4.0 }
println!("{:#?}", p);   // pretty-printed, one field per line
{:#?} is useful during debugging when a struct has many fields.

7.5 Clone and PartialEq

With Clone derived, .clone() produces an independent copy:
let p1 = Point::new(3.0, 4.0);
let p2 = p1.clone();
With PartialEq derived, == compares all fields:
println!("p1 == p2: {}", p1 == p2);  // true
Derive only the traits your struct actually needs - each one adds a small compile-time cost and makes promises about your type's behavior.

7.6 Struct Update Syntax

To create a new instance that differs from an existing one in only a few fields, use .. to fill in the rest:
let p1 = Point::new(3.0, 4.0);
let p3 = Point { x: 1.0, ..p1 };   // y is copied from p1
println!("{:?}", p3);               // Point { x: 1.0, y: 4.0 }
The ..instance part must come last. For fields that are not Copy, this moves them out of p1, leaving p1 partially invalid. Here f64 is Copy, so p1 remains usable.

7.7 Tuple Structs

A tuple struct names the type but not the fields. Fields are accessed by position (self.0, self.1, ...). Use them when the field names would add no information:
#[derive(Debug)]
struct Color(u8, u8, u8);

impl Color {
    fn new(r: u8, g: u8, b: u8) -> Color {
        Color(r, g, b)
    }

    fn is_gray(&self) -> bool {
        self.0 == self.1 && self.1 == self.2
    }
}

let red  = Color::new(255, 0, 0);
let gray = Color::new(128, 128, 128);
println!("{:?}, is_gray: {}", red,  red.is_gray());   // Color(255, 0, 0), is_gray: false
println!("{:?}, is_gray: {}", gray, gray.is_gray());  // Color(128, 128, 128), is_gray: true

7.8 Example - All Together

// Structs - demonstrates struct definitions, impl blocks, derived traits, and tuple structs.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Point { Point { x, y } }
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
    fn translate(&self, dx: f64, dy: f64) -> Point {
        Point { x: self.x + dx, y: self.y + dy }
    }
}

#[derive(Debug)]
struct Color(u8, u8, u8);

impl Color {
    fn new(r: u8, g: u8, b: u8) -> Color { Color(r, g, b) }
    fn is_gray(&self) -> bool { self.0 == self.1 && self.1 == self.2 }
}

fn main() {
    let p1 = Point::new(3.0, 4.0);
    println!("{:?}, distance = {:.2}", p1, p1.distance_from_origin());

    let p2 = p1.clone();
    println!("p1 == p2: {}", p1 == p2);

    let p3 = Point { x: 1.0, ..p1 };
    println!("{:?}", p3);

    let mut p4 = Point::new(5.0, 12.0);
    println!("{:?}, distance = {:.2}", p4, p4.distance_from_origin());
    p4.x = 0.0;
    println!("{:?}", p4);

    let p5 = p4.translate(1.0, -2.0);
    println!("{:?}", p5);

    let red  = Color::new(255, 0, 0);
    let gray = Color::new(128, 128, 128);
    println!("{:?} is_gray: {}", red,  red.is_gray());
    println!("{:?} is_gray: {}", gray, gray.is_gray());
}
Expected output:
Point { x: 3.0, y: 4.0 }, distance = 5.00
p1 == p2: true
Point { x: 1.0, y: 4.0 }
Point { x: 5.0, y: 12.0 }, distance = 13.00
Point { x: 0.0, y: 12.0 }
Point { x: 1.0, y: 10.0 }
Color(255, 0, 0) is_gray: false
Color(128, 128, 128) is_gray: true

7.9 Exercise

Exercise
  • Define a Rectangle struct with width: f64 and height: f64. Add an impl block with new, area(&self) -> f64, perimeter(&self) -> f64, and is_square(&self) -> bool. Derive Debug and print an instance with {:?}.
  • Add a scale(&self, factor: f64) -> Rectangle method that returns a new rectangle with both dimensions multiplied by factor.
  • Define a tuple struct Meters(f64) and a tuple struct Feet(f64). Add a to_feet(&self) -> Feet method to Meters (1 meter = 3.28084 feet). Print a conversion.

7.10 Common Mistakes

Mutating a field on an immutable binding

let p = Point::new(1.0, 2.0);
p.x = 5.0;  // error: cannot assign to `p.x`, as `p` is not declared as mutable
Fix: declare let mut p = ....

Forgetting &self and accidentally consuming self

fn distance(self) -> f64 { ... }  // consumes the instance
A method taking self (not &self) moves the instance into the method. After calling it, the original binding is invalid. Use &self for read-only methods and &mut self for mutating methods.

Deriving PartialEq on a struct containing f64

let a = Point::new(0.1 + 0.2, 0.0);
let b = Point::new(0.3, 0.0);
println!("{}", a == b);  // may print false
Floating-point equality is unreliable due to rounding. For geometry, compare with an epsilon tolerance rather than ==.

Missing fields in the struct literal

let p = Point { x: 1.0 };  // error: missing field `y`
Every field must be supplied unless you use struct update syntax (..other).

7.11 Key Terms

TermMeaning
structA named type grouping one or more fields
fieldA named, typed component of a struct
impl blockAttaches methods and associated functions to a struct
methodA function in an impl block that takes self, &self, or &mut self
associated functionA function in an impl block with no self parameter; called with ::
#[derive]Attribute that auto-implements traits like Debug, Clone, PartialEq
field init shorthandPoint { x, y } when variable names match field names
struct update syntaxPoint { x: 1.0, ..other } to fill remaining fields from another instance
tuple structA struct with unnamed, positionally accessed fields