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 BankAccountmakes the type nameable from outside. It does not make its fields public.pub balance: f64makes that one field public.pub fninside animplblock 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.
- Declare the method inside
impl Dog, belownew:fn get_name(&self) -> &str. The&selfparameter borrows the dog instead of taking it, sorexis still usable afterwards. The return type&stris a borrowed string slice. - Return a reference to the field:
&self.name. This is a&String, and Rust converts it to&strautomatically (deref coercion). Nothing is copied. The function body has noreturnand 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 structto expose the fields. Each field needs its ownpub. - Returning a
Stringwhere a reference is enough.fn get_name(&self) -> Stringwithself.nameas the body will not compile, because it tries to move the field out of a borrowed value. Returning&stravoids that, andself.name.clone()is the option when the caller needs its own copy. - Forgetting
&mut selfon methods that change state.depositmodifiesbalance, so it must borrow mutably, and the variable must be declaredlet mut. - Thinking in classes. A Rust
structholds data and animplblock holds methods. The privacy boundary is the module, not the type.
Key takeaways
- Everything is private by default;
pubopts 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
&stror another reference lets callers read a field without cloning it.
