How to avoid writing duplicate accessor functions for mutable and immutable references in Rust?

一曲冷凌霜 提交于 2019-11-26 15:30:57

You don't, really. Recall that T, &T and &mut T are all different types. In that context, your question is the same as asking "How to avoid writing duplicate accessor functions for String and HashMap".

Matthieu M had the right terms "abstract over the mutability":

The TL;DR is that Rust would likely need to be enhanced with new features to support this. Since no one has succeeded, no one is 100% sure which features those would need to be. The current best guess is higher kinded types (HKT).

(playground links to solutions using type parameters and associated types)

In this case &T and &mut T are just two different types. Code that is generic over different types (at both compile-time and run-time) is idiomatically written in Rust using traits. For example, given:

struct Foo { value: i32 }
struct Bar { foo: Foo }

suppose we want to provide Bar with a generic accessor for its Foo data member. The accessor should work on both &Bar and &mut Bar appropriately returning &Foo or &mut Foo. So we write a trait FooGetter

trait FooGetter {
    type Output;
    fn get(self) -> Self::Output;
}

whose job is to be generic over the particular type of Bar we have. Its Output type will depend on Bar since we want get to sometimes return &Foo and sometimes &mut Foo. Note also that it consumes self of type Self. Since we want get to be generic over &Bar and &mut Bar we need to implement FooGetter for both, so that Self has the appropriate types:

// FooGetter::Self == &Bar
impl<'a> FooGetter for &'a Bar {
    type Output = &'a Foo;
    fn get(self) -> Self::Output { & self.foo }
}

// FooGetter::Self == &mut Bar
impl<'a> FooGetter for &'a mut Bar {
    type Output = &'a mut Foo;
    fn get(mut self) -> Self::Output { &mut self.foo }
}

Now we can easily use .get() in generic code to obtain & or &mut references to Foo from a &Bar or a &mut Bar (by just requiring T: FooGetter). For example:

// exemplary generic function:
fn foo<T: FooGetter>(t: T) -> <T as FooGetter>::Output {
    t.get() 
}

fn main() {
    let x = Bar { foo: Foo {value: 2} };
    let mut y = Bar { foo: Foo {value: 2} };

    foo(&mut y).value = 3;
    println!("{} {}\n", foo(&x).value, foo(&mut y).value);
}

Note that you can also implement FooGetter for Bar, so that get is generic over &T,&mut T, and T itself (by moving it in). This is actually how the .iter() method is implemented in the standard library, and why it always does "the right thing" independently of the reference-ness of the argument its invoked on.

Currently Rust doesn't support abstracting over mutability.

There are some ways this can be achieved, although they aren't ideal:

  • Use a macro to expand the duplicate code, declare the macro and share between both functions - needs to be constructed so it works for mutable and immutable of course.
  • Write the immutable version of the function (to ensure nothing is changed), then write a wrapper function for the mutable version which performs an unsafe cast on the result to make it mutable.

Neither of these are very appealing (a macro is overly verbose and a little less readable, adds some code-bloat), the unsafe is more readable, but it would be nice to avoid since casting from immutable to mutable isn't so nice to have through a code-base.

For now the best option as far as I can see (where copy-pasting code isn't acceptable), is to write an immutable version of the function, then wrap it with a mut version of the function where both inputs and outputs are mutable.

This requires an unsafe cast on the output of the function, so it's not ideal.


Note: it's important to have the immutable function contain the body of the code, since the reverse will allow accidental mutating of what might be immutable input.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!