Structs and impl
In one sentence
Section titled “In one sentence”A struct holds data, impl blocks add functions and methods to it, and there are no classes, no inheritance, and no constructors in the language.
The Java you know
Section titled “The Java you know”A Java class puts fields, constructors, and methods in one block. Access modifiers (private, protected, package-private, public) work per class. A class can extend one other class.
Java
public static final class Order { public final long id; public final String customer; private final List<Long> lines = new ArrayList<>(); // private to the class
public Order(long id, String customer) { // constructor this.id = id; this.customer = customer; }
public long totalCents() { return lines.stream().mapToLong(Long::longValue).sum(); }
public void addLine(long cents) { lines.add(cents); }
public static Order empty(long id) { // static factory return new Order(id, ""); }}Rust
#[derive(Debug, Clone, PartialEq)]pub struct Order { pub id: u64, pub customer: String, lines: Vec<u64>, // private: only code in this module can see it}
impl Order { /// An associated function: no `self`. Call it as `Order::new(...)`. pub fn new(id: u64, customer: &str) -> Self { Self { id, customer: customer.to_string(), lines: Vec::new(), } }
/// `&self`: reads the order. pub fn total_cents(&self) -> u64 { self.lines.iter().sum() }
/// `&mut self`: changes the order. pub fn add_line(&mut self, cents: u64) { self.lines.push(cents); }
/// `self`: consumes the order. The caller cannot use it after the call. pub fn into_lines(self) -> Vec<u64> { self.lines }}The Rust you will see
Section titled “The Rust you will see”Rust splits the class in two:
struct: the fields. Nothing else.impl Order { ... }: functions that belong to the type. A type can have severalimplblocks, also in different files of the same crate.
There is no extends. Shared behavior goes in traits (see Traits vs interfaces), and shared data goes in a field of another struct: composition.
#[derive(Debug, Clone, PartialEq)]pub struct Order { pub id: u64, pub customer: String, lines: Vec<u64>, // private: only code in this module can see it}
impl Order { /// An associated function: no `self`. Call it as `Order::new(...)`. pub fn new(id: u64, customer: &str) -> Self { Self { id, customer: customer.to_string(), lines: Vec::new(), } }
/// `&self`: reads the order. pub fn total_cents(&self) -> u64 { self.lines.iter().sum() }
/// `&mut self`: changes the order. pub fn add_line(&mut self, cents: u64) { self.lines.push(cents); }
/// `self`: consumes the order. The caller cannot use it after the call. pub fn into_lines(self) -> Vec<u64> { self.lines }}- Line 1:
derivegeneratesDebug(a debugtoString),Clone(a copy method), andPartialEq(equals). This replaces Lombok or IDE-generated code. - Line 5: No
pub: the field is private. Private means "private to this module", not "private to this type". See Visibility below. - Line 10:
newis a convention, not a keyword. It is an **associated function**: it has noself, like a Javastaticmethod.Selfis the type of theimplblock. - Line 11-15: A struct literal.
id,is short forid: id,. The function returns the struct because it is the last expression. - Line 19:
&self: a method that reads the object. The caller keeps it. - Line 24:
&mut self: a method that changes the object. The caller needs amutvariable. - Line 29:
self: a method that consumes the object. Afterorder.into_lines(),orderis gone. Java has no equivalent.
At the call site, Type::function calls an associated function, and value.method() calls a method:
let mut order = Order::new(7, "Ada"); // associated function: `Type::name`order.add_line(1_200); // method: `value.name`order.add_line(300);let total = order.total_cents();let lines = order.into_lines(); // `order` is moved; it is gone after this lineHow to read it
Section titled “How to read it”Constructors are conventions
Section titled “Constructors are conventions”Rust has no constructors. A struct literal (Order { id, customer, lines }) is the only way to build a value, and it needs access to every field. So the type’s own module writes functions that build it. You will see these names:
| Name | Meaning |
|---|---|
new(...) |
The main way to build the value. |
with_x(...), from_x(...) |
Other ways, for example Vec::with_capacity(10). |
Default::default() |
A value with every field at its default. #[derive(Default)] makes one. |
builder() |
A builder, for types with many optional settings. |
try_new(...) |
A constructor that can fail: returns Result<Self, E>. |
The struct update syntax ..Self::default() fills the remaining fields from another value. It is the idiomatic way to set “only these fields, defaults for the rest”:
#[derive(Debug, Clone, PartialEq, Default)]pub struct RetryPolicy { pub max_attempts: u32, pub delay_ms: u64, pub log_failures: bool,}
impl RetryPolicy { pub fn new(max_attempts: u32) -> Self { Self { max_attempts, ..Self::default() // the other fields come from `Default` } }
pub fn with_delay(self, delay_ms: u64) -> Self { Self { delay_ms, ..self } }}
pub fn examples() -> [RetryPolicy; 3] { [ RetryPolicy::default(), // every field is zero or false RetryPolicy::new(3).with_delay(500), RetryPolicy { log_failures: true, ..RetryPolicy::new(5) // struct update syntax }, ]}with_delay(self, ...) takes self by value and returns a changed copy. Methods like this can be chained, as in RetryPolicy::new(3).with_delay(500).
Visibility is per module
Section titled “Visibility is per module”Items are private by default. “Private” means visible in the current module and its child modules, not “visible in this type”. You see these levels:
| Rust | Visible in | Closest Java |
|---|---|---|
| (nothing) | this module and its children | package-private, but smaller |
pub(super) |
the parent module | (none) |
pub(crate) |
this crate | package-private, but for the whole library |
pub |
everywhere that the type is reachable | public |
pub mod billing { pub struct Invoice { pub number: String, // visible to all code that can see `Invoice` pub(crate) paid: bool, // visible in this crate only total_cents: u64, // private to the `billing` module }
impl Invoice { pub fn new(number: &str, total_cents: u64) -> Self { Self { number: number.to_string(), paid: false, total_cents, } }
pub fn total_cents(&self) -> u64 { self.total_cents } }
pub fn apply_discount(invoice: &mut Invoice, cents: u64) { if invoice.paid { return; } // Same module, so the private field is visible here, outside `impl`. invoice.total_cents = invoice.total_cents.saturating_sub(cents); }}apply_discount is a free function, outside the impl block, and it can still change the private field, because it is in the same module. Rust has no protected, because it has no subclasses.
Tuple structs and newtypes
Section titled “Tuple structs and newtypes”A tuple struct has fields without names: struct CustomerId(pub u64). The field is .0. A tuple struct with one field is called a newtype. It costs nothing at run time, and it makes the compiler check what a plain u64 cannot:
Java
record CustomerId(long value) {}
record OrderId(long value) {}
static String cancel(CustomerId customer, OrderId order) { return "customer " + customer.value() + " cancels order " + order.value();}Rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]pub struct CustomerId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]pub struct OrderId(pub u64);
pub fn cancel(customer: CustomerId, order: OrderId) -> String { format!("customer {} cancels order {}", customer.0, order.0)}If a caller swaps the two IDs, the code does not compile:
use concepts::structs_and_impl::newtypes::{cancel, CustomerId, OrderId};let customer = CustomerId(42);let order = OrderId(7);cancel(order, customer); // error E0308: mismatched typesJava records give the same safety, but each record is an object on the heap. A Rust newtype is exactly a u64 in memory. See S37: Primitive types where a newtype gives safety (u64 for IDs).
derive: generated code
Section titled “derive: generated code”#[derive(...)] generates trait implementations at compile time. The common ones:
| Derive | Gives you | Java |
|---|---|---|
Debug |
{:?} formatting for logs and tests |
toString() (debug version) |
Clone |
.clone() |
copy constructor |
Copy |
implicit copies, for small types | primitives |
PartialEq, Eq |
== |
equals() |
Hash |
use as a HashMap key |
hashCode() |
PartialOrd, Ord |
<, >, sorting |
Comparable |
Default |
Type::default() |
a no-argument constructor |
Eq and Hash always come together with PartialEq, for the same reason that equals and hashCode do in Java. The derive makes them consistent for you.
What good looks like
Section titled “What good looks like”- Small structs with a clear purpose. Data that belongs together is grouped in its own struct (an
Address, aMoney), not spread over loose fields. - Public fields for plain data, private fields and methods where there is a rule (for example “
reservedis never more thanon_hand”). - Newtypes for IDs and units:
OrderId,CustomerId,Cents. newfor the common case,Defaultand struct update for optional settings, and a builder only when there are many settings or rules between them.- Methods take
&selfwhere they can.&mut selfonly to change,selfonly to consume. - The smallest visibility that works: private, then
pub(crate), thenpub.
Red flags in AI output
Section titled “Red flags in AI output”- Getters and setters for every field.
get_name()andset_name()that only read and write a private field. See S12: Getters and setters for every field. - A
newfunction with many parameters, often several of the same type. See S31: Function with too many parameters (no builder or config struct). pubon every field and function, including helpers that should be private. See S38:pubon everything.u64orStringfor IDs, so that a customer ID can be passed where an order ID is expected. See S37: Primitive types where a newtype gives safety (u64for IDs).- Many
Optionfields that are really states:paid_at,shipped_at,cancelled_at. See S17: Struct with manyOptionfields that are really states. - A “base struct” and
Derefto imitate inheritance. See Traits vs interfaces.
- S12Severity: LowGetters and setters for every fieldJavaBean accessors that add code but no encapsulation.
- S17Severity: MediumStruct with many
Optionfields that are really statesWhich fields are set depends on a hidden state that an enum should express. - S31Severity: LowFunction with too many parameters (no builder or config struct)Long parameter lists that are easy to call in the wrong order.
- S37Severity: LowPrimitive types where a newtype gives safety (
u64for IDs)Two different IDs have the same type, so the compiler cannot catch a mix-up. - S38Severity: Low
pubon everythingEvery field and function is public, so the API has no boundary.
Better vs worse
Section titled “Better vs worse”The task: an address type, and a stock type with a reservation rule.
WorseJavaBean style: private fields, a getter that clones and a setter for each one. 30 lines that protect nothing.
pub struct Address {street: String,city: String,postal_code: String,}impl Address {pub fn new(street: String, city: String, postal_code: String) -> Self {Self {street,city,postal_code,}}pub fn get_street(&self) -> String {self.street.clone()}pub fn set_street(&mut self, street: String) {self.street = street;}pub fn get_city(&self) -> String {self.city.clone()}pub fn set_city(&mut self, city: String) {self.city = city;}pub fn get_postal_code(&self) -> String {self.postal_code.clone()}pub fn set_postal_code(&mut self, postal_code: String) {self.postal_code = postal_code;}}Run in Playground (opens the Rust Playground in a new tab) BetterPublic fields where there is no rule. Private fields and methods where there is one.
/// Plain data with no rules between the fields: public fields.#[derive(Debug, Clone, PartialEq, Eq)]pub struct Address {pub street: String,pub city: String,pub postal_code: String,}/// A rule between fields: private fields, and methods that keep the rule.#[derive(Debug)]pub struct Stock {on_hand: u32,reserved: u32, // never more than `on_hand`}impl Stock {pub fn new(on_hand: u32) -> Self {Self {on_hand,reserved: 0,}}pub fn available(&self) -> u32 {self.on_hand - self.reserved}pub fn reserve(&mut self, quantity: u32) -> Result<(), NotEnoughStock> {let available = self.available();if quantity > available {return Err(NotEnoughStock { available });}self.reserved += quantity;Ok(())}}#[derive(Debug, PartialEq, Eq)]pub struct NotEnoughStock {pub available: u32,}Run in Playground (opens the Rust Playground in a new tab)
The “worse” Address has no rule that its setters enforce, so the private fields add code and no safety. Every get_ call also clones a String. The “better” version uses public fields for Address. For Stock, it keeps the fields private, because reserve must check available() before it changes reserved. That is the case where methods earn their place.
Getters are correct when the field is private for a reason, as in Stock. In Rust they are named after the field (available(), not get_available()), and they return a borrow (&str, &[T]) or a Copy value, not a clone.
Check yourself
Section titled “Check yourself”What is the difference between
Order::new(7, name)andorder.total_cents()?Show the answer
Order::newis an associated function: it has noself, like a Javastaticmethod.order.total_cents()is a method: its first parameter is&self.A method has the signature
fn finish(self) -> Report. What happens to the object?Show the answer
It moves into the method and is consumed. The caller cannot use it after the call. This is how Rust expresses “this object is finished”.
A field has no
pub. Which code can read it?Show the answer
Code in the same module (and its child modules), including free functions and other types in that module. Not only the type’s own
implblock.Why use
struct OrderId(u64)instead ofu64?Show the answer
The compiler then rejects an
OrderIdwhere aCustomerIdis expected. It costs nothing at run time: in memory, it is au64.