【问题标题】:Is there a way to tell rustc when is is being too conservative about certain borrows?有没有办法告诉 rustc 什么时候对某些借款过于保守?
【发布时间】:2022-01-01 07:24:37
【问题描述】:

我正在尝试在 Rust 中构建一个非常简单的基于堆栈的评估器,我遇到了一个奇怪的情况,我认为借用检查器过于保守:

use std::collections::HashMap;

pub type Value = i32;
pub type Result = std::result::Result<(), Error>;
type Op = Box<dyn Fn(&mut Evaluator) -> Result>;
type OpTable = HashMap<String, Op>;

pub struct Evaluator {
    stack: Vec<Value>,
    ops: OpTable,
}

#[derive(Debug, PartialEq)]
pub enum Error {
    DivisionByZero,
    StackUnderflow,
    UnknownWord,
    InvalidWord,
}

impl Evaluator {
    fn add(&mut self) -> Result {
        if let (Some(x), Some(y)) = (self.stack.pop(), self.stack.pop()) {
            self.stack.push(y + x);
            Ok(())
        } else {
            Err(Error::StackUnderflow)
        }
    }

    fn sub(&mut self) -> Result {
        if let (Some(x), Some(y)) = (self.stack.pop(), self.stack.pop()) {
            self.stack.push(y - x);
            Ok(())
        } else {
            Err(Error::StackUnderflow)
        }
    }

    pub fn new() -> Evaluator {
        let stack: Vec<Value> = vec![];
        let mut ops: OpTable = HashMap::new();

        ops.insert("+".to_string(), Box::new(Evaluator::add));
        ops.insert("-".to_string(), Box::new(Evaluator::sub));

        Evaluator { stack, ops }
    }

    pub fn eval(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();

        // user definition
        if let (Some(&":"), Some(&";")) = (symbols.first(), symbols.last()) {
            if symbols.len() > 3 {
                let statement = symbols[2..symbols.len() - 1].join(" ");
                self.ops.insert(
                    symbols[1].to_string().to_ascii_lowercase(),
                    Box::new(move |caller: &mut Evaluator| caller.exec(&statement)),
                );
                return Ok(());
            } else {
                return Err(Error::InvalidWord);
            }
        }
        self.exec(input)
    }

    fn exec(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();
        for sym in symbols {
            if let Ok(n) = sym.parse::<i32>() {
                self.stack.push(n);
            } else {
                let s = sym.to_ascii_lowercase();
                if let Some(f) = self.ops.get(&s) { // <--------------errors here
                    f(self)?; // <----------------------------|
                } else {
                    return Err(Error::InvalidWord);
                }
            }
        }
        Ok(())
    }
}

fn main() {
    let mut e = Evaluator::new();
    e.eval("1 2 +");
    println!("{:?}", e.stack);
    e.eval(": plus-1 1 + ;");
    e.eval("4  plus-1");
    println!("{:?}", e.stack);
}

我明白了:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:77:21
   |
76 |                 if let Some(f) = self.ops.get(&s) {
   |                                  -------- immutable borrow occurs here
77 |                     f(self)?;
   |                     -^^^^^^
   |                     |
   |                     mutable borrow occurs here
   |                     immutable borrow later used by call

For more information about this error, try `rustc --explain E0502`.
error: could not compile `evaluator` due to previous error

我相信这是因为部分哈希图 (f) 不可变地借用了所有 self,然后我将可变地传递 selff()。但是,这里并没有真正的冲突(我认为)。

我可以通过实际删除和重新插入值来解决这个问题:

    fn exec(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();
        for sym in symbols {
            if let Ok(n) = sym.parse::<i32>() {
                self.stack.push(n);
            } else {
                let s = sym.to_ascii_lowercase();

                if self.ops.contains_key(&s) {
                    let f = self.ops.remove(&s).unwrap();
                    if let Err(e) = f(self) {
                        self.ops.insert(s, f);
                        return Err(e);
                    }
                    self.ops.insert(s, f);
                } else {
                    return Err(Error::InvalidWord);
                }
            }
        }
        Ok(())
    }

但这感觉很老套,而且更加冗长和低效。我错过了什么吗?有没有办法告诉编译器第一个版本没问题?

【问题讨论】:

  • 你为什么不让你的Op 将堆栈作为参数而不是整个评估器?
  • 如果f() 删除了ops 的所有元素(包括f),你期望会发生什么?你确定那是安全的吗?
  • 相关阅读:stackoverflow.com/questions/47618823/… 正如其他人所指出的,该代码不安全,因为该操作可能会尝试修改操作本身所在的操作表。编译器正确地阻止了您尝试。重新设计您的操作以直接依赖于堆栈的参数而不是评估器将解决此问题。
  • @DenysSéguret 我最初是这样做的,但遇到了生命周期和闭包的问题。也许我也错过了那里的东西?特别是,将引用移动到用户定义的闭包中会导致 Boxdyn Fn> 被视为Box&lt;[closure]&gt; 的问题。这很奇怪。

标签: rust borrow-checker


【解决方案1】:

编译器完全正确,您的解释也是如此:对get() 的调用需要在self.ops 上借用以返回相同生命周期的&amp;Op。然后,您尝试使用self 的可变借用来调用FnMutself 的这个可变借用别名与self.ops 上的不可变借用别名,理论上这个FnMut 的实现可以修改借用的Opself,这是不允许的。编译器通过别名指针阻止了发生突变的情况。

这种情况在传递&amp;mut self 时经常发生,因为self 的成员上的不可变借用导致更多的借用(&amp;self.ops.get()&amp;self 具有相同的生命周期)“lock”all em> self

虽然您的第二个示例很麻烦,但至少如编译器所证明的那样是正确的:通过从哈希表中删除 OpFnMut 无法再通过 self 到达自身,并且别名时的突变是预防。

更好的方法通常是避免将&amp;mut self 作为参数(&amp;mut self&amp;mut Executor)。

【讨论】:

  • 谢谢,这是有道理的。我将回到尝试传递self 的相关部分。我遇到了一个奇怪的冲突,Box 被视为 Box。如果需要,我会提出另一个问题。
  • 我想我需要将标题更改为“即使你认为自己是对的,有没有办法告诉 rustc 你错了?” :)
猜你喜欢
  • 2016-03-22
  • 2021-11-14
  • 1970-01-01
  • 2011-05-22
  • 2010-11-19
  • 2023-03-29
  • 2020-12-29
  • 2012-09-21
  • 2022-07-13
相关资源
最近更新 更多