首页 > 解决方案 > 在使用匹配臂的关联值之前,如何在匹配语句中使用方法?

问题描述

我正在寻找至少遇到几次的问题的解决方法/解决方案。它发生在匹配结构的枚举成员时,根据匹配,在使用枚举的关联值之前,可能会在结构上调用不同的(变异)方法。这些方法需要对结构的可变引用,这不允许之后使用枚举的关联值。简单的例子:

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
        },
        _ => ()
    }
}

操场:https ://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5a9f8643546d08878bb5fabe5703d889

这是不允许的,因为变体可能已被更改,v甚至可能不再作为值有意义。即使该方法根本不修改变体,也会发生这种情况;只要您必须在使用枚举的关联值之前出于任何原因调用可变方法(例如,改变在计算中使用的具有关联值的不同成员),借用检查器就会错误编译。

我能想到两种解决方法。首先是使用第二个匹配语句,将方法调用与值使用分开。这我不喜欢,因为它分离了逻辑。if let二是在匹配臂内使用嵌套。这稍微好一点,即使到那时我应该是一个相对简单的操作,我有 3 个标签深度。

但是,我首选的解决方案是根本不重新匹配枚举。有没有一种方法可以unsafe在不检查变体的情况下访问枚举的关联值?(或者我可以在调用变异方法后以任何方式避免重新匹配?)

标签: rust

解决方案


理解问题

编译您的代码会产生:

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.varmatch &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();在不可变引用 ( vof 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::V2after 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,、、varTwoVarients是不可变的,只有v是可变的。

这种方法的优点unsafe是编译器可以帮助我们确保遵循我们的规则double()如不小心修改self.var = TwoVariants::V2(...)会导致编译错误。在运行时强制执行借用规则,如果发生违反规则RefCell将立即执行。panic!

使用 RefCell 和 If-Let 解决方案之间的区别

RefCell解决方案和解决方案之间存在一些细微的差异if letif 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()`.");
        }
    },
    _ => ()
}
  1. 我们的规则检查是在运行时执行的,而不是像RefCell解决方案那样在编译时执行。
  2. 如果你做self.var = TwoVariants::V1(NonCopyType { ... })inside VariantHolder::double(),该if let子句仍然会v成功提取。但是,这个提取出来v的不再是原来的了v。如果NonCopyType有超过 1 个字段,这一事实很重要。

推荐阅读