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
}
Source: The Book: Tuple Structs
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 |
|---|---|
|
borrows the instance immutably: read-only methods (the most common) |
|
borrows mutably: methods that modify fields |
|
takes ownership: methods that consume or transform the value ( |
|
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:?}");
}
Source: The Book: Method Syntax
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:
-
newis the primary constructor, cheap and infallible; -
other constructors are named
with_*orfrom_*(Vec::with_capacity,String::from_utf8); -
fallible constructors return
ResultorOption(Isbn::parse); -
types with a meaningful empty or zero value implement
Default, andnew()without arguments should usually equalDefault::default()(Clippy’snew_without_defaultlint 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
}
Source: Rust by Example: New Type Idiom
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 |
|---|---|
|
|
|
|
|
|
|
|
|
use as a |
|
|
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 |
|---|---|
a field is missing from a struct expression |
|
a struct expression names a field that does not exist |
|
no method with that name on the type (or the trait is not in scope) |
|
a private field is accessed from another module |
|
the struct was partially moved by struct update syntax or a field move |
|
calling a |