This post started out as a long-winded answer to a stackoverflow question. The question made me realize that there's a pretty universal way of thinking about mutability when coming from popular languages like C# and Java, and that coming to rust with this mindset often results in confusion and frustration.
I’ll be comparing and contrasting solutions to common problems involving mutability in a way that (hopefully) expresses the differences intuitively. For the non-rust examples, I'll be using Java, but these examples can be expressed similarly in other object-oriented languages.
We'll start with a simple goal—Define a Dog
type with two fields:
- Name, which will be immutable
- Age, which will be mutable
In the first few examples, we'll avoid common idioms like getters and setters in order to keep things simple.
Here's how we'll define our type in Java:
class Dog {
public final String name;
public int age;
public Dog(String name) {
this.name = name;
this.age = 0;
}
}
An equivalent type in rust would look like this:
struct Dog {
pub name: &'static str,
pub age: usize,
}
impl Dog {
pub fn new(name: &'static str) -> Dog {
return Dog { name, age: 0 };
}
}
In the rust example, you may notice that there's no modifier on our name
field to suggest that we're controlling mutability in any way. This is because rust has no concept of mutability at the field level.
Rust takes a fundamentally different approach. If we want to modify any field on our type, we need to declare a mutable variable pointing to it. By default, all variables are immutable and you must add the mut
modifier if you intend to mutate it:
// Declare an immutable variable
let harris = Dog::new("Harris");
harris.age += 1; // Compile error!
// Declare a mutable variable
let mut harris = Dog::new("Harris");
harris.age += 1; // No problem!
At first glance, it would appear as if Java provides finer-grained control over mutability than rust—If a variable's mutable, we can mutate all of its fields; if it's immutable, we can't mutate any of them.
Mutability and self
parameters
Let's update our examples to make them more idiomatic. We'll make our fields private and expose methods to access or modify them.
First in Java:
class Dog {
private final String name;
private int age;
public Dog(String name) {
this.name = name;
this.age = 0;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void incrementAge() {
this.age += 1;
}
}
And our rust example:
struct Dog {
name: &'static str,
age: usize
}
impl Dog {
pub fn new(name: &'static str) -> Dog {
return Dog { name, age: 0 };
}
pub fn get_name(&self) -> &'static str {
return self.name;
}
pub fn get_age(&self) -> usize {
return self.age;
}
pub fn increment_age(&mut self) {
self.age += 1
}
}
In the rust example, the get_name
, get_age
and increment_age
methods all include a self
parameter. Like variables, parameters are either mutable or immutable. self
differs from other parameters in how it's passed to the method: If we have a variable named dog
and we call dog.increment_age()
, the dog
variable is implicitly passed to the increment_age
method as the self
parameter, and can access private fields declared on the type.
This is even more interesting when we consider how this works in conjunction with variables. If we have an immutable variable, and we call a method on it that takes a &mut self
, we'll encounter a compile error because the method requires mutable access to our value!
// Declare an immutable dog variable
let dog = Dog::new("Harris");
dog.increment_age(); // Compile error!
// Declare a mutable dog variable
let mut dog = Dog::new("Harris");
dog.increment_age(); // This works!
So what happens if we declare a variable as mutable, and call a method that takes an immutable self
parameter? In this case, the call is valid, but the method is still only granted immutable access to the self
parameter.
To reiterate: In order to call a method that requires mutable access to self
, the variable must be declared as mutable, but methods that declare an immutable self
parameter can be called via mutable and immutable variables alike.
Encoding mutability in return types
We're going to introduce a new field to illustrate how mutability works with more complex types. We'll add a List
(Vec
in rust) to our example named friends
:
class Dog {
private final List<Dog> friends = new ArrayList<>();
...
public List<Dog> getFriends() {
return this.friends;
}
public void addFriend(Dog friend) {
this.friends.add(friend);
}
}
In the java example, our new friends
field is declared final
, which means we can never change what the field points to, however, we're still free to add and remove items from the list via our getter method as seen below:
Dog harris = new Dog("Harris");
Dog buck = new Dog("Buck");
harris.getFriends().add(buck);
So how would this work in rust? Let's update our example:
struct Dog {
...
friends: Vec<Dog>,
}
impl Dog {
...
pub fn get_friends(&self) -> &Vec<Dog> {
return &self.friends;
}
}
In our rust example, we're actually encoding mutability into the return type of our get_friends
method! The &Vec<Dog>
return type represents an immutable reference because it lacks the mut
keyword. It would be a compile error if we attempted to add an item to the returned friends
list:
let mut harris = Dog::new("Harris");
let buck = Dog::new("Buck");
harris.get_friends().push(buck); // Compile error!
Let's break this down:
- We declared a mutable
harris
variable - The
get_friends
method returns an immutable reference to ourfriends
field - The Vec type has a method named
push
which requires a mutable reference toself
- Our code fails to compile because the immutable reference returned by
get_friends
cannot be used to call a method that requires mutable access toself
So, even though our original variable was declared as mutable, we use the get_friends
method to hide access to our list behind an immutable reference. Consequently, it's a compile error if we attempt to mutate it.
As an exercise, let's say we wanted the get_friends
method to provide mutable access to the the returned friends
reference. Take a moment to consider what we'd need to change.
Have an idea? Here's the answer:
pub fn get_friends(&mut self) -> &mut Vec<Dog> {
&mut self.friends
}
To summarize, we had to:
- Declare our
self
parameter as mutable, - Declare the return type as mutable, and
- Return a mutable reference to our list
We can now modify the friends
list via our get_friends
method! Whether or not we'd ever want to do this in practice is another question.
Conclusion
Mutability in rust is very different from what most people are used to, and it can take some time for it to sink in. Once it does, however, it becomes an incredibly powerful way to enforce guarantees that aren't possible in most languages. It's also a fundamental part of the language, working in concert with other language features to achieve a unique combination of safety and performance.
A quick note about rust idioms
The rust examples aren't particularly idiomatic, but that was done for the sake of clarity and simplicity. Perhaps I'll touch on interesting rust idioms in a separate post!
Top comments (0)