Start learning free

LESSON 402 OF 414 · OBJECT-ORIENTED PROGRAMMING

Access Modifiers in Rust: Private Fields and pub

Lesson Read the full explanation

TL ; DR:

  • Rust has two levels of visibility: private (the default) and pub. There are no public:/private: section labels; you mark each item or field individually.


  • By default, struct fields and methods are private to the module where they are defined; add pub to make them visible to other modules:

    struct Dog {
        name: String,  // private by default (no pub keyword)
    }
    impl Dog {
        fn new(name: &str) -> Dog { Dog { name: name.to_string() } }
        fn get_name(&self) -> &str { &self.name }
    }
    

  • Code in another module can't read rex.name (a compile error), but it can call a pub method such as rex.get_name(). In this lesson everything is in one file, so mainFunction could technically read rex.name too; use get_name() anyway, as that is the habit the lesson teaches.



Full lesson:

Visibility in Rust works at the level of modules: a private item can be used anywhere inside the module that defines it, and pub opens it up to the outside. Keeping fields private and exposing methods lets a struct protect its own data.


struct BankAccount {
    balance: f64,  // private by default
}
impl BankAccount {
    fn deposit(&mut self, amount: f64) {
        if amount < 0.0 { return; }
        self.balance += amount;
    }
    fn get_balance(&self) -> f64 { self.balance }
}

If balance were pub, any client in another module could just write account.balance = -1000.0; and corrupt the state.


Your task

The Dog struct has its name field private by default. Add a fn get_name(&self) -> &str method in the impl block that returns &self.name.

Dog's name: Rex
STEP 1 OF 2
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING

    Access Modifiers in Rust, explained

    What access control means in Rust

    Access control decides which code may read or change a piece of data. The reason to care is simple: if every field is open, any part of a program can put an object into a state its methods never intended. A bank account whose balance anyone can set to -1000.0 is the standard example.

    Rust has no public: or private: labels and no protected level. Instead, every item (struct, field, function, method, module) is private by default, and the keyword pub opens an item up. The privacy is per item, not per section of a block: each field and each method carries its own pub or its absence.

    There is one more difference worth knowing early. Private does not mean "visible only inside impl". A private item is visible inside the module where it is defined, and inside that module's child modules. A module is Rust's unit of encapsulation, and a source file's top level is itself a module.

    Private fields and getter methods

    In the lesson, Dog has a field name with no pub:

    struct Dog {
        name: String,
    }
    

    When Dog is defined in the same module as the code that uses it, as it is in this lesson, that code can still write rex.name directly. Privacy only starts to block access when the struct lives in a different module from its user. That is why real programs put a type in its own module and expose a small set of pub methods.

    Here is a complete example with a module boundary:

    mod bank {
        pub struct BankAccount {
            balance: f64,
            pub owner: String,
        }
    
        impl BankAccount {
            pub fn new(owner: &str) -> BankAccount {
                BankAccount { balance: 0.0, owner: owner.to_string() }
            }
            pub fn deposit(&mut self, amount: f64) {
                if amount < 0.0 { return; }
                self.balance += amount;
            }
            pub fn get_balance(&self) -> f64 { self.balance }
        }
    }
    
    fn main() {
        let mut acc = bank::BankAccount::new("Ann");
        acc.deposit(50.0);
        acc.deposit(-10.0);
        println!("{} {}", acc.owner, acc.get_balance());
    }
    

    This prints:

    Ann 50
    

    The negative deposit is ignored, because the only way to change balance from outside the module is deposit, and deposit checks its argument. The owner field is marked pub, so main reads it directly.

    What the compiler says when you cross the line

    Privacy is checked at compile time, with no cost at run time. If main above tries to write acc.balance = -1000.0;, the program does not build:

    error[E0616]: field `balance` of struct `BankAccount` is private
        acc.balance = -1000.0;
            ^^^^^^^ private field
    

    Calling a private method fails the same way, with error[E0624]: method `audit` is private. Building the struct with a literal such as bank::BankAccount { balance: 1.0 } from outside the module is rejected too (E0451), which is why the module offers a new function. A struct with a private field can only be created by code that can see that field.

    Making things public

    • pub struct BankAccount makes the type nameable from outside. It does not make its fields public.
    • pub balance: f64 makes that one field public.
    • pub fn inside an impl block makes that method callable from outside the module.
    • pub(crate) makes an item visible anywhere in the same crate but not to other crates.

    The lesson's exercise

    The starter code gives Dog a private name, a new function, and a call to rex.get_name() that does not exist yet. Without the method, the program fails to compile with error[E0599]: no method named `get_name` found for struct `Dog` in the current scope. The task is to add it, in two steps.

    1. Declare the method inside impl Dog, below new: fn get_name(&self) -> &str. The &self parameter borrows the dog instead of taking it, so rex is still usable afterwards. The return type &str is a borrowed string slice.
    2. Return a reference to the field: &self.name. This is a &String, and Rust converts it to &str automatically (deref coercion). Nothing is copied. The function body has no return and no semicolon, so the expression is the function's value.

    The finished program:

    struct Dog {
        name: String,
    }
    
    impl Dog {
        fn new(name: &str) -> Dog {
            Dog {
                name: name.to_string(),
            }
        }
    
        fn get_name(&self) -> &str {
            &self.name
        }
    }
    
    fn mainFunction() {
        let rex = Dog::new("Rex");
        println!("Dog's name: {}", rex.get_name());
    }
    

    It prints Dog's name: Rex. Notice that get_name has no pub. Here the caller sits in the same module as Dog, so a private method is visible to it. If Dog lived in its own module, you would write pub fn get_name.

    Common mistakes

    • Expecting pub struct to expose the fields. Each field needs its own pub.
    • Returning a String where a reference is enough. fn get_name(&self) -> String with self.name as the body will not compile, because it tries to move the field out of a borrowed value. Returning &str avoids that, and self.name.clone() is the option when the caller needs its own copy.
    • Forgetting &mut self on methods that change state. deposit modifies balance, so it must borrow mutably, and the variable must be declared let mut.
    • Thinking in classes. A Rust struct holds data and an impl block holds methods. The privacy boundary is the module, not the type.

    Key takeaways

    • Everything is private by default; pub opts in, item by item.
    • Privacy is enforced by the compiler, at the module boundary.
    • Keep fields private and expose methods when you need to guard an invariant such as "the balance is never changed by a negative deposit".
    • A getter that returns &str or another reference lets callers read a field without cloning it.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free