如何避免在 Rust 中为可变和不可变引用编写重复的访问器函数?
How to avoid writing duplicate accessor functions for mutable and immutable references in 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;
}
}
这是一个更详细的代码库摘录,其中不可变的 return 参数被转换为可变的,以支持函数的不可变和可变版本。这使用了包装指针类型(ConstP
和 MutP
用于不可变和可变引用),但是函数的逻辑应该很清楚。
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
、&T
和 &mut T
都是 不同的类型 。在这种情况下,您的问题与询问 "How to avoid writing duplicate accessor functions for String
and HashMap
".
相同
Matthieu M 的条款正确 "abstract over the mutability":
- Parameterisation over mutability
- Dealing with &/&mut in data structures: abstract over mutability or split types?
- A safe way to reuse the same code for immutable and mutable variants of a function?
- Abstracting over mutability in Rust
- "Mutability polymorphism"
- 等等等等等
TL;DR 是 Rust 可能需要通过新功能来增强以支持这一点。由于没有人成功,因此没有人 100% 确定需要哪些功能。目前最好的猜测是 higher kinded types (HKT)。
目前 Rust 不支持对可变性进行抽象。
有一些方法可以实现这一点,尽管它们并不理想:
- 使用宏来扩展重复代码,声明宏并在两个函数之间共享 - 需要构造以便它当然适用于可变和不可变。
- 编写函数的不可变版本(以确保没有任何更改),然后为可变版本编写包装函数,对结果执行
unsafe
转换以使其可变。
这些都不是很吸引人(宏过于冗长且可读性稍差,增加了一些代码膨胀),unsafe
更具可读性,但最好避免使用,因为从通过代码库从不可变到可变并不是那么好。
据我所知,目前最好的选择(复制粘贴代码是不可接受的)是编写函数的不可变版本,然后用 mut
版本包装它输入和输出都可变的函数。
这需要对函数的输出进行 unsafe
强制转换,因此并不理想。
注意:让不可变函数包含代码主体很重要,因为反过来将允许意外改变可能不可变的输入。
(游乐场链接到使用 type parameters and associated types 的解决方案)
在这种情况下,&T
和 &mut T
只是两种不同的类型。在不同类型上通用的代码(在编译时和 运行 时)是使用特征用 Rust 惯用地编写的。例如,给定:
struct Foo { value: i32 }
struct Bar { foo: Foo }
假设我们要为 Bar
的 Foo
数据成员提供通用访问器。访问器应该在 &Bar
和 &mut Bar
上工作,适当地 returning &Foo
或 &mut Foo
。所以我们写一个 trait FooGetter
trait FooGetter {
type Output;
fn get(self) -> Self::Output;
}
其工作是对我们拥有的 Bar
的特定类型进行通用化。它的 Output
类型将取决于 Bar
,因为我们希望 get
有时 return &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()
从 &Bar
或 [=19] 中获取 &
或 &mut
对 Foo
的引用=](只需要 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 T
和 T
本身(通过将其移入)。这实际上就是 .iter()
方法在标准库中的实现方式,以及为什么它总是 "the right thing" 独立于其调用的参数的引用性。
您可以使用 the duplicate
crate:
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;
}
}
这将扩展到您的第一个示例。但是,通常您可能会在代码中使用 constant/mutable 版本的各种调用。因此,这里猜测一下你的第二个例子是如何写的(不得不对命名做出一些猜测):
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();
}
有几次,我 运行 遇到了可变引用和不可变引用都需要访问器方法的情况。
对于大约 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;
}
}
这是一个更详细的代码库摘录,其中不可变的 return 参数被转换为可变的,以支持函数的不可变和可变版本。这使用了包装指针类型(ConstP
和 MutP
用于不可变和可变引用),但是函数的逻辑应该很清楚。
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
、&T
和 &mut T
都是 不同的类型 。在这种情况下,您的问题与询问 "How to avoid writing duplicate accessor functions for String
and HashMap
".
Matthieu M 的条款正确 "abstract over the mutability":
- Parameterisation over mutability
- Dealing with &/&mut in data structures: abstract over mutability or split types?
- A safe way to reuse the same code for immutable and mutable variants of a function?
- Abstracting over mutability in Rust
- "Mutability polymorphism"
- 等等等等等
TL;DR 是 Rust 可能需要通过新功能来增强以支持这一点。由于没有人成功,因此没有人 100% 确定需要哪些功能。目前最好的猜测是 higher kinded types (HKT)。
目前 Rust 不支持对可变性进行抽象。
有一些方法可以实现这一点,尽管它们并不理想:
- 使用宏来扩展重复代码,声明宏并在两个函数之间共享 - 需要构造以便它当然适用于可变和不可变。
- 编写函数的不可变版本(以确保没有任何更改),然后为可变版本编写包装函数,对结果执行
unsafe
转换以使其可变。
这些都不是很吸引人(宏过于冗长且可读性稍差,增加了一些代码膨胀),unsafe
更具可读性,但最好避免使用,因为从通过代码库从不可变到可变并不是那么好。
据我所知,目前最好的选择(复制粘贴代码是不可接受的)是编写函数的不可变版本,然后用 mut
版本包装它输入和输出都可变的函数。
这需要对函数的输出进行 unsafe
强制转换,因此并不理想。
注意:让不可变函数包含代码主体很重要,因为反过来将允许意外改变可能不可变的输入。
(游乐场链接到使用 type parameters and associated types 的解决方案)
在这种情况下,&T
和 &mut T
只是两种不同的类型。在不同类型上通用的代码(在编译时和 运行 时)是使用特征用 Rust 惯用地编写的。例如,给定:
struct Foo { value: i32 }
struct Bar { foo: Foo }
假设我们要为 Bar
的 Foo
数据成员提供通用访问器。访问器应该在 &Bar
和 &mut Bar
上工作,适当地 returning &Foo
或 &mut Foo
。所以我们写一个 trait FooGetter
trait FooGetter {
type Output;
fn get(self) -> Self::Output;
}
其工作是对我们拥有的 Bar
的特定类型进行通用化。它的 Output
类型将取决于 Bar
,因为我们希望 get
有时 return &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()
从 &Bar
或 [=19] 中获取 &
或 &mut
对 Foo
的引用=](只需要 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 T
和 T
本身(通过将其移入)。这实际上就是 .iter()
方法在标准库中的实现方式,以及为什么它总是 "the right thing" 独立于其调用的参数的引用性。
您可以使用 the duplicate
crate:
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;
}
}
这将扩展到您的第一个示例。但是,通常您可能会在代码中使用 constant/mutable 版本的各种调用。因此,这里猜测一下你的第二个例子是如何写的(不得不对命名做出一些猜测):
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();
}