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

时间:2017-01-03 04:40:47

标签: reference rust immutability

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

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

我希望能够重复使用这两种代码。

Rust是否提供了一些方法来处理这个,然后复制粘贴代码或使用unsafe强制转换?

e.g:

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

4 个答案:

答案 0 :(得分:11)

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

Matthieu M有正确的条款&#34;抽象的可变性&#34;:

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

答案 1 :(得分:7)

(游乐场使用type parametersassociated types指向解决方案的链接

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

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

假设我们希望为Bar数据成员提供Foo通用访问者。访问者应该同时&Bar&mut Bar正常返回&Foo&mut Foo。所以我们写了一个特征FooGetter

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

其作用是对我们所拥有的特定类型的Bar具有通用性。其Output类型取决于Bar,因为我们希望get有时会返回&Foo,有时会返回&mut Foo。另请注意,它会使用self类型的Self。由于我们希望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()&&mut获取对Foo的{​​{1}}或&Bar次引用(只需要&mut Bar)。例如:

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); } 实施FooGetter,以便Bar通用get&T&mut T本身(通过移动它)。这实际上是T方法在标准库中的实现方式,以及为什么它总是独立于其调用的参数的引用而做“正确的事情”。

答案 2 :(得分:0)

您可以使用the duplicate crate

use duplicate::duplicate;

impl MyStruct {
  #[duplicate(
    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;
#[duplicate(
  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();
}

答案 3 :(得分:-6)

目前Rust并不支持抽象可变性。

虽然它们并不理想,但仍有一些方法可以实现:

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

这些都不是很吸引人(宏过于冗长,可读性稍差,添加了一些代码膨胀),unsafe更具可读性,但是从不可变到强制转换会很好通过代码库,可变性并不是很好。

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

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

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