如何解决可变和不可变借用的共存?

xxk*_*kkk 3 rust borrow-checker

我有一个Context结构:

struct Context {
    name: String,
    foo: i32,
}

impl Context {
    fn get_name(&self) -> &str {
        &self.name
    }
    fn set_foo(&mut self, num: i32) {
        self.foo = num
    }
}

fn main() {
    let mut context = Context {
        name: "MisterMV".to_owned(),
        foo: 42,
    };
    let name = context.get_name();
    if name == "foo" {
        context.set_foo(4);
    }
}
Run Code Online (Sandbox Code Playgroud)

在一个函数中,我需要首先得到namecontextfoo根据name我的更新:

let name = context.get_name();
if (name == "foo") {
    context.set_foo(4);
}
Run Code Online (Sandbox Code Playgroud)

代码将无法编译,因为get_name()take &selfset_foo()take &mut self.换句话说,我有一个不可变的借用,get_name()但我也在set_foo()同一范围内有可变借用,这违反了引用规则.

在任何给定的时间,您可以拥有(但不是两个)一个可变引用或任意数量的不可变引用.

错误看起来像:

error[E0502]: cannot borrow `context` as mutable because it is also borrowed as immutable
  --> src/main.rs:22:9
   |
20 |     let name = context.get_name();
   |                ------- immutable borrow occurs here
21 |     if name == "foo" {
22 |         context.set_foo(4);
   |         ^^^^^^^ mutable borrow occurs here
23 |     }
24 | }
   | - immutable borrow ends here
Run Code Online (Sandbox Code Playgroud)

我想知道如何解决这种情况?

Lin*_*ope 9

这是一个非常广泛的问题.借用检查器可能是Rust最有用的功能之一,也是最棘手的处理方式.人体工程学的改进正在定期进行,但有时会出现这种情况.

有几种方法可以解决这个问题,我会尝试回顾每个方法的优点和缺点:

I.转换为仅需要有限借款的表格

当你学习Rust时,你会慢慢了解借阅到期以及有多快.例如,在这种情况下,您可以转换为

if context.get_name() == "foo" {
    context.set_foo(4);
}
Run Code Online (Sandbox Code Playgroud)

借用在if语句中到期.这通常是您想要的方式,并且随着非词汇生命周期等功能变得更好,此选项变得更加可口.例如,当NLL可用时,您当前编写它的方式将起作用,因为这种结构被正确地检测为"有限借用"!由于奇怪的原因(例如,如果声明需要连接可变和不可变的调用),重构有时会失败,但应该是您的首选.

II.使用带有表达式作为语句的作用域攻击

let name_is_foo = {
    let name = context.get_name();
    name == "foo"
};

if name_is_foo {
    context.set_foo(4);
}
Run Code Online (Sandbox Code Playgroud)

Rust使用任意返回值的任意范围语句的能力非常强大.如果其他一切都失败了,你几乎总是可以使用块来限制借用,并且只返回一个非借用标志值,然后用于可变调用.方法I通常更清楚,但是这个方法很有用,清晰,并且惯用Rust.

III.在类型上创建"融合方法"

   impl Context {
      fn set_when_eq(&mut self, name: &str, new_foo: i32) {
          if self.name == name {
              self.foo = new_foo;
          }
      }
   }
Run Code Online (Sandbox Code Playgroud)

当然,这有无穷无尽的变化.最常见的是一个函数,它接受一个fn(&Self) -> Option<i32>,并根据该闭包的返回值进行None设置(对于"不设置",Some(val)设置该值).

有时最好允许结构修改自己而不用"外部"逻辑.对于树木尤其如此,但在最坏的情况下会导致方法爆炸,当然如果在无法控制的外来类型上操作,则无法实现.

IV.克隆

let name = context.get_name().clone();
if name == "foo" {
    context.set_foo(4);
}
Run Code Online (Sandbox Code Playgroud)

有时您必须快速克隆.尽可能避免这种情况,但有时候只是投入clone()某个地方而不是花费20分钟试图弄清楚如何使你的借用工作是值得的.取决于您的截止日期,克隆的成本,您调用该代码的频率,等等.

例如,可以说PathBuf在CLI应用程序中过度克隆s并不是非常罕见的.

V.使用不安全(不推荐)

let name: *const str = context.get_name();
unsafe{
    if &*name == "foo" {
        context.set_foo(4);
    }
}
Run Code Online (Sandbox Code Playgroud)

这应该几乎从来没有被使用,但在极端情况下可能是必要的,或在情况下的性能,其中你基本上被迫克隆(这可以用图形或一些靠不住的数据结构发生).总是,总是尽量避免这种情况,但请将其保存在工具箱中,以备不时之需.

请记住,编译器期望您编写的不安全代码支持安全Rust代码所需的所有保证.一个unsafe块表示虽然编译器无法验证代码是否安全,但程序员有.如果程序员没有正确验证它,编译器可能会产生包含未定义行为的代码,这可能导致内存不安全,崩溃等等,Rust努力避免的许多事情.