The difference between getting a reference to a struct field via a method and a direct field reference.(mutable/immutable borrowing)

Hi, I'm trying to figure out the difference between getting a reference to a struct field via a method of that struct.

here is the code

I have two methods update_widgets_fail and update_widgets_success. These methods do the same job, but the first uses the struct method to obtain field reference and the second does it via direct field reference.
Could anyone enlighten me as to why the update_widgets_fail method throws a borrow error but the second one doesn't?

I know that it is because of the self argument in the reference-getter, but I have no idea why it works in such a way.

The most important principle here is local reasoning of the borrow-checker. If there’s a call to get_widgets, the borrow checker will never inspect what’s going on inside of that function. It only works with the function signature:

fn get_widgets(&self) -> &Vec<MyWidget>

which is equivalent to (with explicit, not elided lifetime marks)

fn get_widgets<'a>(&'a self) -> &'a Vec<MyWidget>

The borrow checker interprets such a signature to the effect of treating the resulting &'a Vec<MyWidget> as something that – essentially – still constitutes a borrow of all of the MyStruct value, like the &'a self argument it got as input; the result is as you’ll notice, that you cannot also borrow self.name_collector mutably at the same time.

As for why: Local reasoning of the borrow checker is a really important thing. Borrow-checking is hard enough to understand as-is, and it would get impossibly hard if it started reporting errors that involve the function body of many different functions at once. Additionally, this helps with encapsulation and API stability. What exactly happens inside of a function should not leak to the outside (at least not more than necessary); changing mere implementation details shouldn’t break API users (it’s already hard-enough to avoid accidental semver-violations as-is).

But okay… getters aren’t exactly the most complex thing in the world… if the borrow-checker wants an accurate description of what’s going on (w.r.t. borrows) in the function signature, what’s the “correct” signature for a getter instead? Answer: As of now, there is no better signature to choose, but it needn’t always be this way. People have certainly already thought about the issue of making function signatures more expressive.

Finally, practical solutions/approaches on this limitation:

  • #1: don’t write getters! When a field is public (which seems to be the case with the widgets field here), that’s the API, no need for a getter. If a field is private, but has getters, check whether it can be made public instead. (Public fields are always mutable; so e.g. concerns of holding up some invariants can be a good argument against making a field public.)
  • or, if you need getters, try offering “borrow-splitting” API. Make a &mut self method that returns some struct or tuple containing references to all accessible fields at once
4 Likes

An alternative to thinking in terms of local reasoning is to think in terms of taking the borrows that the signature demands. For example, people often wish for or expect an interface that allows something like this:

impl<T> Collection<T> {
    // the important one
    fn insert<'s>(&'s mut self, element: T) -> &'s T { /* ... */ }
    fn get<'s>(&'s self, idx: usize) -> Option<&'s T> { /* ... */ }
}

fn elsewhere(c: &mut Collection<String>) {
    let two = c.insert(String::new());
    let one = c.get(1).unwrap();
    println!("{one} v. {two}");
}

But &mut doesn't work that way; the collection remains exclusively borrowed so long as the &T returned from insert is in use. &mut inputs don't downgrade to &.

This never really clicked for me until I started thinking about the inputs and outputs a little separately. This API:

    fn insert<'s>(&'s mut self, element: T) -> &'s T { /* ... */ }

says, "in order to get a &'s T, you must first create a &'s mut Self (of the same duration!)". Once you've created the exclusive borrow of Self for 's, there's no going back, as far as the borrow checker is concerned. So the collection remains exclusively borrowed, and (as of yet) we have no mechanism at the API level to "downgrade" the &mut after the method returns.[1]

Similarly, a &[mut] Self receiver always means "in order to call this method, you must borrow all of Self", because that's what a &[mut] Self is, and the method signature demands creating one in order to be called.

Here's another article that talks about some workarounds on stable when "don't use getters" isn't an adequate fix.[2]


  1. You can "manually downgrade" in the method, with an awkward API. ↩︎

  2. The article is a precursor to the language-level "view types" article @steffahn linked. ↩︎

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.