Structs
Structs group named fields under a single type. Rust gives you three flavours — classic, tuple, and unit — plus derive macros for the common traits (Debug, Clone, PartialEq) that make idiomatic Rust code so terse.
Define, methods, traits, derive, update
EXAMPLE
// 1) Classic struct
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
println!("({}, {})", p.x, p.y);
}
// 2) Tuple struct — fields by position, no names
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
println!("R={}", red.0);
// Useful for newtype patterns:
struct UserId(u64);
let uid = UserId(42);
// 3) Unit struct — zero fields, useful for markers / state types
struct Authenticated;
struct Anonymous;
struct Session<S> { user_id: Option<u64>, _state: std::marker::PhantomData<S> }
// 4) Derive common traits
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Coord {
x: i32,
y: i32,
}
fn main2() {
let a = Coord { x: 1, y: 2 };
let b = a; // Copy types are duplicated automatically
println!("{:?}", a); // 'Coord { x: 1, y: 2 }'
println!("{:?}", a == b); // true
}
// Copy is for cheap-to-copy fields only (no allocations).
// Clone is required for everything else; .clone() explicitly duplicates.
// 5) Methods — impl block
impl Coord {
fn new(x: i32, y: i32) -> Self { // associated function (constructor-style)
Self { x, y }
}
fn distance(&self, other: &Coord) -> f64 { // method
let dx = (self.x - other.x) as f64;
let dy = (self.y - other.y) as f64;
(dx * dx + dy * dy).sqrt()
}
fn translate(&mut self, dx: i32, dy: i32) { // mutable method
self.x += dx;
self.y += dy;
}
}
fn main3() {
let mut p = Coord::new(1, 2);
p.translate(3, 4);
let q = Coord::new(0, 0);
println!("distance = {}", p.distance(&q));
}
// 6) Struct update syntax — clone with overrides
let base = Coord { x: 1, y: 2 };
let shifted = Coord { x: 10, ..base }; // y: 2 from base, x: 10 overridden
// 7) Field init shorthand
fn make(name: String, age: u32) -> Person {
Person { name, age } // shorthand when field name == variable name
}
struct Person { name: String, age: u32 }
// 8) Public vs private fields
pub struct ApiResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub(crate) raw_body: Vec<u8>, // visible inside crate only
cookies: Vec<String>, // module-private
}
// Convention: keep fields private; expose getters/setters or builder pattern.
// 9) Builder pattern
pub struct HttpClient {
timeout: std::time::Duration,
retries: u32,
user_agent: String,
}
pub struct HttpClientBuilder {
timeout: std::time::Duration,
retries: u32,
user_agent: String,
}
impl HttpClient {
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder {
timeout: std::time::Duration::from_secs(30),
retries: 0,
user_agent: "my-app/1.0".into(),
}
}
}
impl HttpClientBuilder {
pub fn timeout(mut self, t: std::time::Duration) -> Self { self.timeout = t; self }
pub fn retries(mut self, n: u32) -> Self { self.retries = n; self }
pub fn user_agent(mut self, ua: impl Into<String>) -> Self { self.user_agent = ua.into(); self }
pub fn build(self) -> HttpClient {
HttpClient { timeout: self.timeout, retries: self.retries, user_agent: self.user_agent }
}
}
let client = HttpClient::builder()
.timeout(std::time::Duration::from_secs(5))
.retries(3)
.build();
// 10) Generic structs
struct Pair<T, U> { first: T, second: U }
impl<T: Clone, U: Clone> Pair<T, U> {
fn duplicate(&self) -> (Pair<T, U>, Pair<T, U>) {
(Pair { first: self.first.clone(), second: self.second.clone() },
Pair { first: self.first.clone(), second: self.second.clone() })
}
}
// 11) Lifetimes — struct holding a reference
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self, prefix: &str) -> String {
format!("{} {}", prefix, self.part)
}
}
fn main_lifetime() {
let s = String::from("the quick brown fox");
let words: Vec<&str> = s.split_whitespace().collect();
let e = Excerpt { part: words[0] };
println!("{}", e.announce("first:"));
}
// 12) Default trait — for cheap default values
#[derive(Default, Debug)]
struct Config {
host: String,
port: u16,
debug: bool,
}
let c = Config::default(); // String::new(), 0, false
let c2 = Config { port: 8080, ..Default::default() };
// 13) Serialisation — serde
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Address {
line1: String,
city: String,
country: String,
}
let a: Address = serde_json::from_str(r#"{\"line1\": \"1 Pitt St\", \"city\": \"Sydney\", \"country\": \"AU\"}"#).unwrap();
let json = serde_json::to_string(&a).unwrap();
// 14) Struct vs enum
// • struct — fixed shape; every value has all the same fields
// • enum — choice between variants (Result, Option, your own DTOs)
// • If you find yourself with optional fields that only apply in some cases, an enum may fit better
// 15) Common bugs
// • Trying to mutate a borrowed struct → 'cannot borrow as mutable'; use &mut
// • Forgot 'pub' on a field → 'field is private' from outside the module
// • Derive Copy on a struct with String → won't compile; String is owned heap data
// • Lifetimes on every method param → most need only one; lifetime elision handles common cases
// • Public fields drift from invariants — prefer constructors that validate
// • Using struct update syntax with a non-Copy field → moves; can't use original after
// • Forgetting Hash + Eq when used as HashMap key — won't compile
Why it matters
Structs group fields; impl blocks add methods. Derive the common traits (Debug, Clone, PartialEq) you’ll inevitably need, keep fields private and expose builders or constructors that validate invariants, and use enums when a single shape can’t express “variant A or variant B” cleanly.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
struct User { name: String, age: u32 }
impl User {
fn new(name: &str, age: u32) -> Self {
Self { name: name.into(), age }
}
}
Try it Yourself »
Discussion
Loading…