Skip to content

Structs and impl

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.

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
}
}

Rust splits the class in two:

  • struct: the fields. Nothing else.
  • impl Order { ... }: functions that belong to the type. A type can have several impl blocks, 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
}
}
  1. Line 1: derive generates Debug (a debug toString), Clone (a copy method), and PartialEq (equals). This replaces Lombok or IDE-generated code.
  2. Line 5: No pub: the field is private. Private means "private to this module", not "private to this type". See Visibility below.
  3. Line 10: new is a convention, not a keyword. It is an **associated function**: it has no self, like a Java static method. Self is the type of the impl block.
  4. Line 11-15: A struct literal. id, is short for id: id,. The function returns the struct because it is the last expression.
  5. Line 19: &self: a method that reads the object. The caller keeps it.
  6. Line 24: &mut self: a method that changes the object. The caller needs a mut variable.
  7. Line 29: self: a method that consumes the object. After order.into_lines(), order is 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 line

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).

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.

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:

Does not compile: E0308
use concepts::structs_and_impl::newtypes::{cancel, CustomerId, OrderId};
let customer = CustomerId(42);
let order = OrderId(7);
cancel(order, customer); // error E0308: mismatched types

Java 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(...)] 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.

  • Small structs with a clear purpose. Data that belongs together is grouped in its own struct (an Address, a Money), not spread over loose fields.
  • Public fields for plain data, private fields and methods where there is a rule (for example “reserved is never more than on_hand”).
  • Newtypes for IDs and units: OrderId, CustomerId, Cents.
  • new for the common case, Default and struct update for optional settings, and a builder only when there are many settings or rules between them.
  • Methods take &self where they can. &mut self only to change, self only to consume.
  • The smallest visibility that works: private, then pub(crate), then pub.

The task: an address type, and a stock type with a reservation rule.

  1. 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)
  2. 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.

  1. What is the difference between Order::new(7, name) and order.total_cents()?

    Show the answer

    Order::new is an associated function: it has no self, like a Java static method. order.total_cents() is a method: its first parameter is &self.

  2. 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”.

  3. 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 impl block.

  4. Why use struct OrderId(u64) instead of u64?

    Show the answer

    The compiler then rejects an OrderId where a CustomerId is expected. It costs nothing at run time: in memory, it is a u64.