DEV Community

Cover image for Mutability in Rust, and how it differs from object-oriented languages
W. Brian Gourlie
W. Brian Gourlie

Posted on

Mutability in Rust, and how it differs from object-oriented languages

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 };
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Let's break this down:

  • We declared a mutable harris variable
  • The get_friends method returns an immutable reference to our friends field
  • The Vec type has a method named push which requires a mutable reference to self
  • Our code fails to compile because the immutable reference returned by get_friends cannot be used to call a method that requires mutable access to self

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
    }
Enter fullscreen mode Exit fullscreen mode

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)