Structs
Group related data, give it a name, attach behavior. The default container in Rust.
A struct is the default way to model a "thing with parts" in Rust. It holds named fields, you build methods on top with impl blocks, and the compiler keeps the layout predictable. Most Sail data types are structs. Most of your data types should be too.
Defining and instantiating
pub struct User {
pub id: u64,
pub email: String,
pub active: bool,
}
let alice = User {
id: 1,
email: String::from("alice@example.com"),
active: true,
};
println!("{}", alice.email);Field access uses dot syntax. Mutating a field requires the whole struct to be mut:
let mut alice = User { id: 1, email: String::from("a@b.com"), active: true };
alice.active = false;Rust does not have per-field mutability. The whole instance is mutable or it is not. If you need a mix, use interior mutability (Cell, RefCell, Mutex) for the specific field, not the whole struct.
The struct update syntax fills in remaining fields from another instance:
let bob = User {
id: 2,
email: String::from("bob@example.com"),
..alice
};..alice must come last. Beware: it moves fields out of alice if they are not Copy. After this, alice.email is gone if email was the only non-Copy field copied through.
Tuple structs and the newtype pattern
Tuple structs use positional fields:
pub struct Point(pub f64, pub f64);
let origin = Point(0.0, 0.0);
let x = origin.0;The big use case is the newtype pattern: wrap a primitive so the type system tracks intent.
pub struct UserId(pub u64);
pub struct OrderId(pub u64);
fn fetch_user(id: UserId) -> Option<User> { /* ... */ }
let oid = OrderId(42);
fetch_user(oid); // compile error: expected UserId, found OrderIdTwo u64 values cannot be confused now. The runtime cost is zero. The compile-time safety is real. This is one of the most useful patterns in Rust, and one agents reach for too rarely.
Unit-like structs
A struct with no fields:
pub struct AlwaysEqual;
impl PartialEq for AlwaysEqual {
fn eq(&self, _other: &Self) -> bool { true }
}Useful as a marker type, as a zero-sized phantom for trait implementations, or as a sentinel. Not common in business logic, but you will see them in libraries.
Methods via impl blocks
Methods live in an impl block, not inside the struct definition:
impl User {
// Associated function (no self). Used as User::new(...).
pub fn new(id: u64, email: String) -> Self {
Self { id, email, active: true }
}
// Method with shared borrow. Read-only.
pub fn display_name(&self) -> &str {
&self.email
}
// Method with exclusive borrow. Mutates self.
pub fn deactivate(&mut self) {
self.active = false;
}
// Method that consumes self. Caller cannot use the value after.
pub fn into_email(self) -> String {
self.email
}
}Three method signatures, three meanings:
&selfreads. The caller can keep using the value, and other shared borrows can exist.&mut selfmutates. Exclusive while the call runs.selfconsumes. Ownership transfers into the method. The caller loses the value.
Pick the weakest signature that works. &self first, &mut self only if you must mutate, self only if you genuinely take over the value (often when converting to something else).
Multiple impl blocks
You can split methods across multiple impl blocks. The compiler treats them as one:
impl User {
pub fn new(id: u64, email: String) -> Self { /* ... */ }
}
impl User {
pub fn deactivate(&mut self) { /* ... */ }
}Common reasons to split: grouping by feature, gating some methods behind #[cfg(...)] flags, or attaching different generic bounds in different blocks. Sail uses this constantly: one impl block for inherent methods, separate blocks for each trait the type implements.
Derive macros
Rust generates trait implementations from a single attribute:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct User {
pub id: u64,
pub email: String,
pub active: bool,
}That one line gave you:
Debugfor{:?}formatting.Clonefor.clone().PartialEqandEqfor==.Hashfor use as aHashMapkey.
The fields must themselves implement these traits, or the derive fails. String implements all of them, so User derives them too.
Display vs Debug
Two formatting traits, two purposes:
Debugis for programmers.{:?}prints structure. Derived.Displayis for users.{}prints a meaningful representation. Hand-written.
use std::fmt;
#[derive(Debug)]
pub struct Money {
pub cents: i64,
pub currency: String,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let dollars = self.cents / 100;
let cents = (self.cents % 100).abs();
write!(f, "{}.{:02} {}", dollars, cents, self.currency)
}
}println!("{:?}", m) prints Money { cents: 1050, currency: "USD" }. println!("{}", m) prints 10.50 USD. Two different audiences.
Never derive Display. It does not exist as a derive (intentional: there is no obvious "user-friendly" representation for arbitrary data). If a type should be Display, you write the impl.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Session {
pub user_id: u64,
pub token: String, // compile error: String is not Copy
}#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Session {
pub user_id: u64,
pub token: String,
}
// Implement Default manually if "empty session" is meaningful;
// often it is not, and you should require explicit construction.