Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 25 of 26

Easy Primitives

pub on structs and enums behaves differently

A shape module owns a rectangle type whose fields nobody outside may touch. Two functions are graded.

pub fn rect_areas(dims: Vec<(i64, i64)>) -> Vec<Option<u64>>
pub fn orientations(dims: Vec<(i64, i64)>) -> Vec<String>

A rectangle is only valid if both sides are strictly positive and both fit in a u32. rect_areas returns the area for valid dimensions and None otherwise; orientations returns portrait, landscape, square, or invalid.

Private fields are how you hold an invariant

Rect stores u32s and promises they are non-zero. That promise is only worth anything if the only way to build a Rect is through Rect::new, which checks. Private fields are what makes the constructor the sole entry point:

mod shape {
    pub struct Rect { w: u32, h: u32 }    // pub type, private fields
    impl Rect {
        pub fn new(w: i64, h: i64) -> Option<Rect> { /* validates */ }
    }
}

Outside the module, Rect { w: 3, h: 4 } is E0451 (“field w of struct Rect is private”) and rect.w is E0616 (“field w of struct Rect is private”). The starter ships the second one. Inside the module, both work normally — privacy in Rust is module-scoped, and child modules can see their ancestors’ private items.

This is the moment privacy stops feeling like bureaucracy. It is not about hiding things from colleagues; it is the only mechanism in the language for saying “this combination of field values cannot occur, and I have made it impossible to create one.”

The asymmetry

Here is the fact that catches everyone:

  • pub struct leaves every field private unless you mark each one pub.
  • pub enum makes every variant public, automatically, with no way to hide one.

That looks inconsistent until you ask what each would be for. A struct with public fields has no invariants — anyone can write any value into any field — so private-by-default is the useful default. An enum with private variants would be unusable: you could not match on it, could not construct it, could not do anything. The defaults differ because the useful answer differs.

A consequence worth knowing: ..Default::default() does not bypass privacy. Struct update syntax is still a struct literal, and a struct literal outside the defining module cannot name private fields, whether it lists them or not.

pub, and the other visibilities

pub means “visible wherever the containing module is”. There is also pub(crate) (this crate only — the workhorse for internal APIs), pub(super) (the parent module) and pub(in path). Reaching for a private item from outside is E0603, “module shape is private” or “function new is private”.

Validating at the boundary

Rect::new takes i64 and returns Option<Rect> — wide input, narrow storage, explicit failure. u32::try_from(w) is the tool: it returns Err for negatives and for anything above u32::MAX, which is exactly the two ways the caller can be wrong. Compare w as u32, which would silently turn -1 into 4294967295.

Notice how the types make the shape of the design visible from outside: the module’s public surface is new -> Option<Rect>, area -> u64, orientation -> Orientation, and there is no fourth way to reach the data.

Lints for library authors

partial_pub_fields (allow-by-default) flags a struct where some fields are pub and some are not, on the grounds that the type has not decided whether it is a data bag or an abstraction. exhaustive_structs and exhaustive_enums (both restriction) force you to opt into #[non_exhaustive], which the next item covers.

Remember the grade is compile + tests + clippy -D warnings.