Structs

Encapsulation

Encapsulation in Rust hides a struct's internal representation behind a module boundary so that external code can only construct, inspect, and mutate the struct through its public methods, guaranteeing that representation invariants can never be violated.

Module-Based Privacy Boundary

Unlike C++ or Java (where privacy is per-class), Rust's privacy boundary is the module:

  • Inside the defining module (and its child submodules): Code can access all private fields and private methods of a struct directly.
  • Outside the defining module: Code can only access fields and methods explicitly marked pub (or pub(crate) / pub(super)).

If a struct has even one private field, code outside its defining module cannot:

  1. Construct the struct using a struct literal (Type { .. }).
  2. Destructure the struct completely without .. (or read the private field).
  3. Mutate the private field directly (instance.private_field = x).
Module Privacy Boundary Around `BoundedScore`:

  mod scoring {
      pub struct BoundedScore {
          value: u8,   <-- Private field: guaranteed to be in `0..=100`
      }
      impl BoundedScore {
          pub fn new(v: u8) -> Option<Self> { .. }  <-- Enforces invariant at creation
          pub fn add(&mut self, d: u8)      { .. }  <-- Preserves invariant on mutation
          pub fn get(&self) -> u8           { .. }  <-- Safe read-only access
      }
  }
  Outside `mod scoring`: Direct literal `BoundedScore { value: 255 }` is a compile error!

Enforcing Multi-Field Invariants Through Encapsulation

When a struct maintains an invariant across multiple fields (for example, min <= max in a range or keeping a running sum synchronized with a vector of entries), making those fields private prevents external callers from putting the struct into an inconsistent state:

mod stats {
    #[derive(Debug)]
    pub struct RunningAverage {
        count: usize, // Private: always equals the number of recorded samples
        sum: f64,     // Private: always equals the sum of recorded samples
    }

    impl RunningAverage {
        pub fn new() -> Self {
            Self { count: 0, sum: 0.0 }
        }

        pub fn record(&mut self, sample: f64) {
            self.count += 1;
            self.sum += sample;
        }

        pub fn count(&self) -> usize {
            self.count
        }

        pub fn mean(&self) -> Option<f64> {
            if self.count == 0 {
                None
            } else {
                Some(self.sum / self.count as f64)
            }
        }
    }
}

fn main() {
    let mut avg = stats::RunningAverage::new();
    println!("Empty mean: {:?}", avg.mean());

    avg.record(10.0);
    avg.record(20.0);
    avg.record(30.0);

    println!("Samples: {}, Mean: {:?}", avg.count(), avg.mean());
}
Empty mean: None
Samples: 3, Mean: Some(20.0)

Evolving Internal Representation Without Breaking Callers

Because external code cannot access private fields, you can change a struct's internal data structure—for example, replacing celsius: f64 with kelvin: f64—without breaking any downstream code that uses its public methods.

Preventing Exhaustive Construction and Matching with #[non_exhaustive]

Sometimes you want all fields of a configuration or event struct to be pub for convenient field access, while still reserving the right to add new fields in future minor versions without breaking callers' struct literals or destructuring patterns. Applying the #[non_exhaustive] attribute to a struct disables direct struct literals and exhaustive destructuring outside the defining crate:

#[non_exhaustive]
#[derive(Debug)]
pub struct BuildOptions {
    pub release: bool,
    pub jobs: usize,
}

impl BuildOptions {
    pub fn default_options() -> Self {
        Self {
            release: false,
            jobs: 4,
        }
    }
}

fn main() {
    let mut opts = BuildOptions::default_options();
    opts.release = true;

    // External crates destructuring a `#[non_exhaustive]` struct must include `..`:
    let BuildOptions { release, .. } = opts;
    println!("Release build: {release}");
}
Release build: true