Structs & Methods

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A struct groups related values into a named type with named (or positional) fields, and impl blocks attach behaviour to it. Together with enums (Enums & Pattern Matching) and traits (Traits), structs are how Rust models data — there are no classes and no inheritance. Examples use the Book and Isbn types of the shelf catalogue.

Defining and instantiating structs

A struct with named fields lists each field with its type. Instances are created with a struct expression that gives every field a value, in any order; leaving one out is error E0063, naming a field that does not exist is E0560.

#[derive(Debug)]
struct Book {
    title: String,
    author: String,
    year: u16,
    available: bool,
}

fn new_book(title: &str, author: &str, year: u16) -> Book {
    Book {
        title: title.to_owned(),
        author: author.to_owned(),
        year,              // field-init shorthand for `year: year`
        available: true,
    }
}

fn main() {
    let mut dune = new_book("Dune", "Frank Herbert", 1965);
    dune.available = false;                    // the whole binding must be `mut`
    println!("{} ({}) by {}", dune.title, dune.year, dune.author);
    println!("{dune:?}");
    println!("{dune:#?}");                     // pretty-printed Debug output
}

Mutability applies to the whole instance: Rust has no per-field mut. If one field must change while the rest stay fixed, either keep it private behind a method, or use interior mutability (see Interior Mutability & Lazy Initialization).

Struct update syntax and moves

..other fills the remaining fields from another instance. It is a sequence of field moves: fields of Copy types are copied, but any moved non-Copy field makes the source partially moved:

#[derive(Debug, Clone)]
struct Book {
    title: String,
    author: String,
    year: u16,
}

fn main() {
    let first = Book { title: "Dune".into(), author: "Frank Herbert".into(), year: 1965 };
    let sequel = Book { title: "Dune Messiah".into(), year: 1969, ..first };
    // `author` (a String) was moved out of `first`; `first.title` and `first.year` are still usable:
    println!("{} {} / {sequel:?}", first.title, first.year);
    // println!("{first:?}"); // would not compile: borrow of partially moved value (E0382)

    let reprint = Book { year: 2005, ..sequel.clone() }; // clone first to keep `sequel` intact
    println!("{reprint:?} {sequel:?}");
}

Tuple structs and unit structs

A tuple struct has positional fields, accessed as .0, .1, …​ It gives a name — and a distinct type — to a tuple. A unit struct has no fields at all and is useful as a marker or as a type that implements a trait without holding data.

#[derive(Debug, Clone, Copy, PartialEq)]
struct Rgb(u8, u8, u8);

#[derive(Debug, Clone, Copy, PartialEq)]
struct Meters(f64);

#[derive(Debug)]
struct AlwaysAvailable; // unit struct: zero-sized

fn main() {
    let cover = Rgb(200, 40, 40);
    let Rgb(r, _, _) = cover;           // destructure
    let shelf_width = Meters(1.2);
    println!("{cover:?} red={r} width={} {:?}", shelf_width.0, AlwaysAvailable);
    println!("{}", size_of::<AlwaysAvailable>()); // 0
}

Methods and impl blocks

Behaviour is defined in impl blocks. A function inside impl whose first parameter is a receiver is a method; the receiver determines what the method may do with the instance:

Receiver Meaning

&self (short for self: &Self)

borrows the instance immutably: read-only methods (the most common)

&mut self

borrows mutably: methods that modify fields

self

takes ownership: methods that consume or transform the value (into_*, builders)

self: Box<Self>, self: Rc<Self>, self: Arc<Self>, self: Pin<&mut Self>

the instance behind that smart pointer

Self (capital S) is an alias for the type the impl block is for.

#[derive(Debug)]
struct Shelf {
    label: String,
    titles: Vec<String>,
    capacity: usize,
}

impl Shelf {
    /// Associated function used as a constructor: no receiver, called as `Shelf::new(...)`.
    fn new(label: &str, capacity: usize) -> Self {
        Self { label: label.to_owned(), titles: Vec::with_capacity(capacity), capacity }
    }

    fn is_full(&self) -> bool {                            // reads
        self.titles.len() >= self.capacity
    }

    fn add(&mut self, title: &str) -> Result<(), String> { // modifies
        if self.is_full() {
            return Err(format!("shelf {} is full", self.label));
        }
        self.titles.push(title.to_owned());
        Ok(())
    }

    fn into_titles(self) -> Vec<String> {                  // consumes
        self.titles
    }
}

fn main() {
    let mut shelf = Shelf::new("A1", 2);
    shelf.add("Dune").unwrap();
    shelf.add("Emma").unwrap();
    println!("{:?}", shelf.add("Kindred"));    // Err("shelf A1 is full")
    let titles = shelf.into_titles();          // `shelf` is moved
    println!("{titles:?}");
}

Method calls auto-reference: shelf.add(...) is rewritten to Shelf::add(&mut shelf, ...) because add takes &mut self. There is no -> operator as in C and C++; . works on values and references alike (see the method-call algorithm on Type Conversions & Coercions). Calling a method that does not exist on the type is error E0599; often the fix is to import the trait that provides it.

A type can have several impl blocks, and they can live in different modules of the same crate — useful for grouping methods, or for impl blocks guarded by #[cfg(...)].

Associated functions and constructors

Functions in impl without a receiver are associated functions, called with Type::function(). Rust has no special constructor syntax; by convention:

  • new is the primary constructor, cheap and infallible;

  • other constructors are named with_* or from_* (Vec::with_capacity, String::from_utf8);

  • fallible constructors return Result or Option (Isbn::parse);

  • types with a meaningful empty or zero value implement Default, and new() without arguments should usually equal Default::default() (Clippy’s new_without_default lint checks this).

#[derive(Debug, Default, Clone, PartialEq)]
struct SearchOptions {
    query: String,
    limit: usize,          // 0 by default
    include_lent: bool,    // false by default
}

impl SearchOptions {
    fn new(query: &str) -> Self {
        Self { query: query.to_owned(), limit: 20, ..Default::default() }
    }
}

fn main() {
    let defaults = SearchOptions::default();
    let custom = SearchOptions { include_lent: true, ..SearchOptions::new("dune") };
    println!("{defaults:?}\n{custom:?}");
}

For types with many optional settings, a builder gives a more readable API; see Idioms, Patterns & API Guidelines.

Visibility of fields

Struct fields are private by default: only code in the same module (and its children) can read, write or name them. Mark fields pub (or pub(crate)) to expose them. A struct with any private field cannot be constructed with a struct expression outside its module — the only way in is a constructor, which is how types enforce invariants. Accessing a private field from outside is error E0616. See Modules, Crates & Visibility.

The newtype pattern: Isbn

A newtype is a tuple struct with a single field that wraps another type to give it a distinct identity and its own invariants. It costs nothing at run time — Isbn has exactly the size and layout of a String — but the compiler now refuses to pass an arbitrary string where an ISBN is expected. shelf-core defines its Isbn like this (simplified):

use std::fmt;

/// A validated ISBN-13, stored without hyphens.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Isbn(String); // private field: only `parse` can create one

impl Isbn {
    pub fn parse(text: &str) -> Result<Self, String> {
        let digits: String = text.chars().filter(|c| *c != '-' && *c != ' ').collect();
        if digits.len() != 13 || !digits.bytes().all(|b| b.is_ascii_digit()) {
            return Err(format!("not an ISBN-13: {text}"));
        }
        let sum: u32 = digits
            .bytes()
            .enumerate()
            .map(|(i, b)| u32::from(b - b'0') * if i % 2 == 0 { 1 } else { 3 })
            .sum();
        if !sum.is_multiple_of(10) {
            return Err(format!("bad check digit: {text}"));
        }
        Ok(Self(digits))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for Isbn {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

fn main() {
    let isbn = Isbn::parse("978-0-441-01359-3").expect("valid");
    println!("{isbn} {:?}", Isbn::parse("978-0-441-01359-4"));
    println!("{}", size_of::<Isbn>() == size_of::<String>()); // true
}

Newtypes are also how you implement a foreign trait for a foreign type (the orphan rule; see Traits) and how you give units of measure distinct types (Meters versus Feet).

Derived traits

#[derive(...)] generates standard trait implementations from the struct’s fields. The common set for a plain data type is:

Derive Gives you

Debug

{:?} and {:#?} formatting

Clone, Copy

.clone(), and implicit copies if every field is Copy

PartialEq, Eq

== and != comparing every field

PartialOrd, Ord

<, >, sort(): lexicographic by field order

Hash

use as a HashMap/HashSet key (together with Eq)

Default

Type::default() with every field’s default

Each derive requires the same trait on every field. The rules and laws are covered on Standard Traits & Operator Overloading; serialization with #[derive(Serialize, Deserialize)] is on Serialization with serde.

References in structs

A struct can hold references if it declares lifetime parameters (struct Record<'a> { title: &'a str }). Such a struct is a view that cannot outlive the data it borrows. Use them for short-lived parsers and iterators; use owned fields for anything stored long-term. Azam (Advanced Rust, chapter 3) discusses references in structs and the borrow limits they create in method chains; the details are on Lifetimes and Borrowing & References.

Common errors

Error Cause

E0063

a field is missing from a struct expression

E0560

a struct expression names a field that does not exist

E0599

no method with that name on the type (or the trait is not in scope)

E0616

a private field is accessed from another module

E0382

the struct was partially moved by struct update syntax or a field move

E0596

calling a &mut self method on a non-mut binding