如何避免在Rust中为可变和不可变引用编写重复的访问器函数



有几次,我遇到过这样的场景:可变和不可变引用都需要访问器方法。

对于大约3行,复制逻辑不是问题,但当逻辑变得更复杂时,复制粘贴大块代码是不好的。

我希望能够为这两种情况重用代码。

Rust是否提供了比复制粘贴代码或使用unsafe强制转换更好的处理方法?

例如:

impl MyStruct {
pub fn get_foo(&self) -> &Bar {
// ~20 lines of code
// --- snip ---
return bar;
}
pub fn get_foo_mut(&mut self) -> &mut Bar {
// ~20 lines of code
// (exactly matching previous code except `bar` is mutable)
// --- snip ---
return bar;
}
}

这里是一个代码库的更详细摘录,其中不可变的返回参数被强制转换为mutable,以支持函数的不可变和可变版本。这使用了一个封装的指针类型(ConstPMutP用于不可变和可变引用),但函数的逻辑应该很清楚。

pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP
where V: Into<VertConstP>,
F: Into<FaceConstP>
{
into_expand!(f, v);
let l_first = f.l_first.as_const();
let mut l_iter = l_first;
loop {
if l_iter.v == v {
return l_iter;
}
l_iter = l_iter.next.as_const();
if l_iter == l_first {
break;
}
}
return null_const();
}
pub fn face_vert_share_loop_mut(f: FaceMutP, v: VertMutP) -> LoopMutP {
let l = face_vert_share_loop(f, v);
return unsafe {
// Evil! but what are the alternatives?
// Perform an unsafe `const` to `mut` cast :(
// While in general this should be avoided,
// its 'OK' in this case since input is also mutable.
l.as_mut()
};
}

(操场链接到使用类型参数和相关类型的解决方案)

在这种情况下,&T&mut T只是两种不同的类型。在不同类型上通用的代码(在编译时和运行时)通常是使用traits在Rust中编写的。例如,给定:

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

假设我们想为BarFoo数据成员提供一个通用访问器。访问者应该同时处理&Bar&mut Bar,并适当地返回&Foo&mut Foo。所以我们写了一个特征FooGetter

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

其工作是在我们所具有的特定类型的CCD_ 13上是通用的。它的Output类型将取决于Bar,因为我们希望get有时返回&Foo,有时返回&mut Foo。还要注意,它消耗类型为Selfself。由于我们希望get&Bar&mut Bar上是通用的,因此我们需要为两者实现FooGetter,以便Self具有适当的类型:

// 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 }
}

现在,我们可以很容易地在泛型代码中使用.get()来从&Bar&mut Bar获得对Foo&&mut引用(只需要T: FooGetter)。例如:

// 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);
}

请注意,您还可以为Bar实现FooGetter,因此get&T&mut TT本身的泛型(通过将其移入)。这实际上就是.iter()方法在标准库中的实现方式,也是为什么它总是做"正确的事情",而不依赖于它调用的参数的引用性。

你真的没有。回想一下,T&T&mut T都是不同的类型。在这种情况下,您的问题与询问"如何避免为StringHashMap编写重复的访问器函数"相同。

Matthieu M有正确的术语"抽象而非可变性":

  • 可变性上的参数化
  • 处理&&数据结构中的mut:抽象过度可变性还是拆分类型
  • 对于函数的不可变和可变变体,重复使用相同代码的安全方法是什么
  • Rust中对可变性的抽象
  • "突变多态性">
  • 等等。等等

TL;DR表示,Rust可能需要通过新功能进行增强以支持这一点。由于没有人成功,没有人100%确定需要哪些功能。目前的最佳猜测是更高级的类型(HKT)。

您可以使用duplicate机箱:

use duplicate::duplicate_item;
impl MyStruct {
#[duplicate_item(
get_foo         self        return_type;
[get_foo]       [&self]     [&Bar];
[get_foo_mut]   [&mut self] [&mut Bar]
)]
pub fn get_foo(self) -> return_type {
// ~20 lines of code
// --- snip ---
return bar;
}
}

这将扩展到您的第一个示例。然而,通常您可能会在代码中使用各种调用的常量/可变版本。因此,这里有一个关于如何编写第二个示例的猜测(必须对命名进行一些猜测):

use duplicate::duplicate_item;
#[duplicate_item(
face_vert_share_loop        VertConstP    FaceConstP    LoopConstP    as_const    null_const;
[face_vert_share_loop]      [VertConstP]  [FaceConstP]  [LoopConstP]  [as_const]  [null_const];
[face_vert_share_loop_mut]  [VertMutP]    [FaceMutP]    [LoopMutP]    [as_mut]    [null_mut];
)]
pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP
where V: Into<VertConstP>,
F: Into<FaceConstP>
{
into_expand!(f, v);

let l_first = f.l_first.as_const();
let mut l_iter = l_first;
loop {
if l_iter.v == v {
return l_iter;
}

l_iter = l_iter.next.as_const();
if l_iter == l_first {
break;
}
}

return null_const();
}

将扩展到:

pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP
where
V: Into<VertConstP>,
F: Into<FaceConstP>,
{
into_expand!(f, v);
let l_first = f.l_first.as_const();
let mut l_iter = l_first;
loop {
if l_iter.v == v {
return l_iter;
}
l_iter = l_iter.next.as_const();
if l_iter == l_first {
break;
}
}
return null_const();
}
pub fn face_vert_share_loop_mut<V, F>(f: F, v: V) -> LoopMutP
where
V: Into<VertMutP>,
F: Into<FaceMutP>,
{
into_expand!(f, v);
let l_first = f.l_first.as_mut();
let mut l_iter = l_first;
loop {
if l_iter.v == v {
return l_iter;
}
l_iter = l_iter.next.as_mut();
if l_iter == l_first {
break;
}
}
return null_mut();
}

当前Rust不支持对可变性进行抽象。

有一些方法可以实现这一点,尽管它们并不理想:

  1. 使用宏来扩展重复的代码,声明宏并在两个函数之间共享-需要进行构造,这样它当然适用于可变和不可变
  2. 编写该函数的不可变版本(以确保不发生任何更改),然后为可变版本编写一个包装函数,该包装函数对结果执行unsafe强制转换以使其可变

这两个都不太吸引人(宏过于冗长,可读性稍差,增加了一些代码膨胀),unsafe更可读,但最好避免,因为从不可变到可变的转换不太适合通过代码库。

目前,就我所见(复制粘贴代码是不可接受的),最好的选择是编写一个不可变版本的函数,然后用mut版本的函数包装它,其中输入和输出都是可变的。

这需要在函数的输出上进行unsafe强制转换,因此这并不理想。


注意:让不可变函数包含代码主体是很重要的,因为反过来会允许不可变输入的意外变化。

最新更新