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

时间:2018-10-08 19:48:26

标签: 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);
    }
}

在一个函数中,我需要首先获取name中的context并根据我拥有的foo更新name

let name = context.get_name();
if (name == "foo") {
    context.set_foo(4);
}

由于get_name()占用&selfset_foo()占用&mut self,因此代码无法编译。换句话说,我在get_name()处拥有不可变的借项,但在同一范围内却在the rules of references范围内拥有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

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

3 个答案:

答案 0 :(得分:9)

这是一个非常广泛的问题。借阅检查器也许是Rust的最有用的功能之一,但也是处理起来最棘手的问题。人体工程学的改进正在定期进行,但有时会发生这种情况。

有几种处理方法,我将尝试每种方法的利弊:

I。转换为只需要有限借用的表格

在学习Rust的过程中,您会慢慢了解借阅到期的时间和速度。例如,在这种情况下,您可以转换为

if context.get_name() == "foo" {
    context.set_foo(4);
}

借款在if语句中到期。 通常是您要采用的方式,并且随着诸如非词法生存期之类的功能变得越来越好,此选项也变得更加可口。例如,当NLL可用时,由于该构造被正确检测为“有限借用”,因此您当前编写的方式将起作用!重新编写有时会由于奇怪的原因而失败(特别是如果一条语句需要可变和不可变的调用相结合时),但它应该是您的首选。

II。将expression-as-statement用作范围界定黑客

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

if name_is_foo {
    context.set_foo(4);
}

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;
          }
      }
   }

当然,这有无尽的变化。最通用的函数是接受fn(&Self) -> Option<i32>并根据该闭包的返回值进行设置的函数({None表示“不设置”,Some(val)设置该值)。

有时最好允许结构修改自身而不执行逻辑“外部”。对于树木而言尤其如此,但在最坏的情况下可能导致方法爆炸,并且如果在不受您控制的异类上进行操作,当然是不可能的。

IV。克隆

let name = context.get_name().clone();
if name == "foo" {
    context.set_foo(4);
}

有时您必须进行快速克隆。尽可能避免这种情况,但有时值得将clone()放在某个地方,而不是花20分钟试图弄清楚如何使您的借钱工作。取决于您的截止日期,克隆的价格,调用该代码的频率等等。

例如,可以说在CLI应用程序中过度克隆PathBuf并不少见。

V。使用不安全的(不推荐

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

几乎不应该使用此方法,但是在极端情况下或在本质上不得不克隆的情况下(对于图形或某些不稳定的数据结构可能会发生),为提高性能可能是必需的。始终要尽最大的努力避免这种情况,但应将其保存在工具箱中,以防万一。

请记住,编译器期望您编写的不安全代码可以维护安全Rust代码所需的所有保证。 unsafe块表示,虽然编译器无法验证代码是否安全,但程序员可以。如果程序员没有对其进行正确验证,则编译器可能会生成包含未定义行为的代码,这可能导致内存不安全,崩溃等,Rust努力避免这些事情。

答案 1 :(得分:4)

可能有一些答案已经回答了您,但是在很多情况下会触发此错误消息,因此我将回答您的具体情况。

更简单的解决方案是使用#![feature(nll)],它将毫无问题地进行编译。

但是您可以使用以下简单匹配方法来解决此问题,而无需使用nll:

fn main() {
    let mut context = Context {
        name: "MisterMV".to_owned(),
        foo: 42,
    };
    match context.get_name() {
        "foo" => context.set_foo(4),
        // you could add more case as you like
        _ => (),
    }
}

答案 2 :(得分:2)

在看到@Stargateur的评论之前,我想出了下面的代码,该代码可以正常编译,但是会克隆名称字符串:

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

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

fn main() {
    let mut context = Context {
        name: String::from("bill"),
        foo: 5,
    };

    let name = context.get_name();
    if name == "foo" {
        context.set_foo(4);
    }
    println!("Hello World!");
}

使用@Stargateur的示例,事实证明,有一个非常简单的解决此特定问题的方法-与get_nameif结合使用,例如

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

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

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

我相信这是因为get_name部分的变量的生存期已清楚地标出,而name变量是单独的,则其值实际上可能会突然改变而无需显式更改。