Rust Getters and Setters

· 339 words · 2 minute read

Avoid getters and setters in Rust. Thanks to Java, I reflexively add getters and setters regardless of language. But I recently started to understand why it’s better to avoid them in Rust. Suppose you’re building a calendar:

struct Day {
    n: u8,
    is_moon_full: bool,
}

struct Month {
    days_in_month: Vec<Day>,
    blue_moon: bool,
}

impl Default for Month {
    // not shown: populating days_in_month
    fn default() -> Self {
        Self {
            days_in_month: Vec::default(),
            blue_moon: false,
        }
    }
}

fn main() {
  let mut m = Month::default();
  let mut saw_full_moon = false;
  for day in m.days_in_month {
    if day.is_moon_full {
        if saw_full_moon {
            m.blue_moon = true;
        }
        saw_full_moon = true;
      }
  }
  eprintln!("Month contains blue moon: {}", m.blue_moon);
}

There are problems with this code, but it works well enough as an illustration. Let’s ironically improve it with a setter:

impl Month {
    fn set_blue_moon(&mut self, blue_moon: bool) {
        self.blue_moon = blue_moon;
    }
}

// ...

if saw_full_moon {
    m.set_blue_moon(true);
}

This innocuous change gives us a build error:

error[E0382]: borrow of partially moved value: `m`
  --> src/main.rs:33:13

From the borrow checker’s perspective, there is indeed a big difference. The first version sets a field. The second version calls a method. The borrow checker can easily reason about the effects of setting a field: the field changes. But calling a mutable method could do much more, so the correct scope of the borrow is the struct, not the field. It’s easy to look at the body of a typical setter and see how limited its effects are. But the signature of the method makes no such promise.

Now that you’re thinking about a method’s signature as distinct from its body, you’re primed to read Steve Klabnik’s article, Rust’s Golden Rule. It’s central to Rust that borrow checker stops its call analysis at the signature. Once you internalize this, you’ll see that an innocent-looking setter is far too big a tool for the job of mutating a single field, which is why its benefits in Rust rarely outweigh its costs.

Playground code