rust - 在使用匹配臂的关联值之前,如何在匹配语句中使用方法?
问题描述
我正在寻找至少遇到几次的问题的解决方法/解决方案。它发生在匹配结构的枚举成员时,根据匹配,在使用枚举的关联值之前,可能会在结构上调用不同的(变异)方法。这些方法需要对结构的可变引用,这不允许之后使用枚举的关联值。简单的例子:
struct NonCopyType {
foo: u32
}
enum TwoVariants {
V1(NonCopyType),
V2(NonCopyType)
}
struct VariantHolder {
var: TwoVariants
}
impl VariantHolder {
fn double(&mut self) {
match &mut self.var {
TwoVariants::V1(v) => {
v.foo *= 2;
},
TwoVariants::V2(v) => {
v.foo *= 2;
}
}
}
}
fn main() {
let var = TwoVariants::V1( NonCopyType {
foo: 1
});
let mut holder = VariantHolder {
var
};
match &mut holder.var {
TwoVariants::V1(v) => {
holder.double();
println!("{}", v.foo); // Problem here
},
_ => ()
}
}
这是不允许的,因为变体可能已被更改,v
甚至可能不再作为值有意义。即使该方法根本不修改变体,也会发生这种情况;只要您必须在使用枚举的关联值之前出于任何原因调用可变方法(例如,改变在计算中使用的具有关联值的不同成员),借用检查器就会错误编译。
我能想到两种解决方法。首先是使用第二个匹配语句,将方法调用与值使用分开。这我不喜欢,因为它分离了逻辑。if let
二是在匹配臂内使用嵌套。这稍微好一点,即使到那时我应该是一个相对简单的操作,我有 3 个标签深度。
但是,我首选的解决方案是根本不重新匹配枚举。有没有一种方法可以unsafe
在不检查变体的情况下访问枚举的关联值?(或者我可以在调用变异方法后以任何方式避免重新匹配?)
解决方案
理解问题
编译您的代码会产生:
error[E0499]: cannot borrow `holder` as mutable more than once at a time
--> temp.rs:38:13
|
36 | match &mut holder.var {
| --------------- first mutable borrow occurs here
37 | TwoVariants::V1(v) => {
38 | holder.double();
| ^^^^^^ second mutable borrow occurs here
39 | println!("{}", v.foo); // Problem here
| ----- first borrow later used here
所以我们不能有 2 个可变借用。请注意,我们没有修改holder.var
,我们可以摆脱不可变的引用。改成match &mut holder.var
并match &holder.var
编译,我们得到:
error[E0502]: cannot borrow `holder` as mutable because it is also borrowed as immutab
le
--> temp.rs:38:13
|
36 | match &holder.var {
| ----------- immutable borrow occurs here
37 | TwoVariants::V1(v) => {
38 | holder.double();
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
39 | println!("{}", v.foo); // Problem here
| ----- immutable borrow later used here
因此编译器会阻止我们holder.double();
在不可变引用 ( v
of holder.var
) 仍在使用时执行突变 ( )。就像你提到的,
这是不允许的,因为变体可能已被更改,并且 v 甚至可能不再作为值有意义。
但是,我们程序员制定了以下规则:holder.double()
只能修改v
;所有其他字段应保持不变。例如holder.double()
可以做v.foo = 13
,但不能做self.var = TwoVariants::V2(...)
。如果遵循规则v
,调用后访问应该没有问题holder.double()
,因为它仍然是相同的v
,只是v.foo
改变了。
v
现在的问题是,调用后如何访问holder.double()
?
不安全的解决方案
正如 L. Riemer 在评论中指出的那样,您可以使用具有不安全结构的原始指针。match
将函数中的表达式修改为main
以下代码,它应该可以编译:
match &holder.var {
TwoVariants::V1(v) => {
// Create a pointer pointing to v.
let pv = v as *const NonCopyType;
holder.double();
// Dereference the pointer, then create a reference to v.
let v = unsafe { &*pv };
// Access v as usual.
println!("{}", v.foo);
},
_ => ()
}
请注意,强烈反对这种方法,因为编译器无法保证pv
编译时指向的数据的有效性,也没有运行时错误检测。我们只是假设v
从取消引用中得到的是pv
原始的v
并且holder.double()
将始终遵循规则。
为了说明这一点,尝试使用修改后的编译VariantHolder::double()
:
fn double(&mut self) {
match &mut self.var {
TwoVariants::V1(v) => {
v.foo *= 2;
// Assume that we accidentally perform some operations that modify
// self.var into TwoVariants::V2.
self.var = TwoVariants::V2(NonCopyType { foo: v.foo + 1 });
},
TwoVariants::V2(v) => {
v.foo *= 2;
}
}
}
我们可以看到它编译得很好。3
如果你运行它会被打印出来,这意味着v
它实际上是TwoVariants::V2
after call的一个元素holder.double()
,而不是原来v
的。
这种编译良好且不产生运行时错误的错误很难发现、确定和修复。如果你在系统中添加堆分配和线程,事情会变得更加复杂,谁知道哪个操作会破坏规则并失效pv
。
一种(安全的)RefCell 解决方案
回想一下,我们的规则只允许修改v
. 一种解决方法是使用内部可变性模式std::cell::RefCell
:
use std::cell::RefCell;
struct NonCopyType {
foo: u32
}
enum TwoVariants {
// Wrap NonCopyType in RefCell, since this is the start of modification
// point allowed by our *rule*.
V1(RefCell<NonCopyType>),
V2(RefCell<NonCopyType>)
}
struct VariantHolder {
var: TwoVariants
}
impl VariantHolder {
// Remove mut, telling the compiler that the `double()` function does not
// need an exclusive reference to self.
fn double(&self) {
match &self.var {
TwoVariants::V1(v) => {
// Borrow mutable from v and modify it.
v.borrow_mut().foo *= 2;
},
TwoVariants::V2(v) => {
v.borrow_mut().foo *= 2;
}
}
}
}
fn main() {
// Create a RefCell to contain NonCopyType.
let var = TwoVariants::V1(RefCell::new(NonCopyType {
foo: 1
}));
let mut holder = VariantHolder {
var
};
match &holder.var {
TwoVariants::V1(v) => {
// Now `double()` only borrow immutably from `holder`, fixing the
// "borrow as mutable while immutable reference is still alive"
// problem.
holder.double();
// Borrow from v.
let v = v.borrow();
// Access v as usual.
println!("{}", v.foo);
},
_ => ()
}
}
本质上,我们是在告诉编译器我们的规则,即在double()
函数中holder
,、、var
和TwoVarients
是不可变的,只有v
是可变的。
这种方法的优点unsafe
是编译器可以帮助我们确保遵循我们的规则。double()
如不小心修改self.var = TwoVariants::V2(...)
会导致编译错误。在运行时强制执行借用规则,如果发生违反规则RefCell
将立即执行。panic!
使用 RefCell 和 If-Let 解决方案之间的区别
RefCell
解决方案和解决方案之间存在一些细微的差异if let
。if let
解决方案可能如下所示:
match &holder.var {
TwoVariants::V1(v) => {
holder.double();
// Use if-let to unpack and get v from holder.var.
if let TwoVariants::V1(v) = &holder.var {
// Access v as usual.
println!("{}", v.foo);
} else {
panic!("*Rule* violated. Check `holder.double()`.");
}
},
_ => ()
}
- 我们的规则检查是在运行时执行的,而不是像
RefCell
解决方案那样在编译时执行。 - 如果你做
self.var = TwoVariants::V1(NonCopyType { ... })
insideVariantHolder::double()
,该if let
子句仍然会v
成功提取。但是,这个提取出来v
的不再是原来的了v
。如果NonCopyType
有超过 1 个字段,这一事实很重要。
推荐阅读
- aws-cdk - CDK 兼容是什么意思?
- google-chrome - 如何解决“无法建立连接,接收端不存在”?
- python - Python 和 Win10 - 运行命令并在完成后重复
- tensorflow - 为什么我的自定义损失(分类交叉熵)不起作用?
- r - 如何在向量列表上使用 expand.grid?
- python - 如何将 ctypes argtypes c_ubytes 用于 unsigned char *?
- javascript - 当我们改变类型值时触发替换
- bash - 使用 Git Bash 命令行从一个 biz zip 文件中解压缩多个 zip 文件
- python - Python 3 SQL 二进制数据插入而不转换
- javascript - 如何映射具有未知嵌套级别的数组?