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

8

我有一个 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);
    }
}

在一个函数中,我需要首先获取contextname,并根据我拥有的name更新foo
let name = context.get_name();
if (name == "foo") {
    context.set_foo(4);
}

代码无法编译,因为 get_name() 接受 &selfset_foo() 接受 &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

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


2
只是一条注释,我更正了您的&String -> &str,这是String的借用类型。我不打算更改您的方法名称,但请知道通常Rust获取器的风格仅为变量名称,而不是get_variable,因此是Context::name(&self)而不是Context::get_name(&self)。无大碍,只是想让您知道。 - Linear
@LinearZoetrope 感谢您的评论。我只是好奇在 Rust 中是否有关于 setter 的约定? - xxks-kkk
我认为 set_x 是标准用法,但我不确定。个人经验来看,更常见的是使用 x_mut,它返回一个可变引用到该变量。(但显然有时你可能需要一个 setter 而不是暴露原始变量)。 - Linear
3个回答

13

这是一个非常广泛的问题。借用检查器可能是Rust最有帮助的功能之一,但也是最麻烦的。人们正在定期改进人体工程学,但有时会发生这种情况。

处理此问题有几种方法,我将尝试介绍每种方法的优缺点:

I. 转换为仅需要有限借用的形式

在学习Rust时,您会逐渐了解何时以及多快会过期。例如,在这种情况下,您可以转换为

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

在 if 语句中,借用将会过期。通常情况下这是你想要的方式,随着非词法生命周期等功能的不断优化,这个选项也变得更加可取。例如,当存在非词法生命周期时,你目前编写的方式将起作用,因为这种构造被正确地检测为“有限借用”!重新构造有时会因奇怪的原因而失败(特别是如果语句需要可变和不可变调用的连接),但应该是你的首选。

II. 使用表达式作为语句的作用域黑科技

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

if name_is_foo {
    context.set_foo(4);
}

Rust的能力在任意作用域语句中返回值是非常强大的。如果所有其他方法都失败了,您几乎可以总是使用块来限定您的借用,并且只返回一个非可借用标志值,然后将其用于可变调用。当有方法I.可用时,通常最好采用该方法,但这个方法也是有用的、清晰的和符合Rust惯例的。

III. 在类型上创建“fused method”

   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. 使用unsafe(不建议使用

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

这几乎永远不应该被使用,但在极端情况下或在必须克隆(例如图形或某些奇怪的数据结构)时可能是必需的。始终尽最大努力避免使用,但如果您确实必须使用,请将其保存在工具箱中。

请记住,编译器期望您编写的不安全代码符合安全Rust代码所需的所有保证。 unsafe 块表示,虽然编译器无法验证代码的安全性,但程序员已经进行了验证。如果程序员没有正确验证它,则编译器很可能会生成包含未定义行为的代码,这可能导致内存不安全、崩溃等等问题,而这正是Rust努力避免的事情之一。


有了非词法生命周期,您将不需要将代码转换为仅需要有限借用的形式。启用NLLs后,OP的代码可以无需修改即可编译,因为自动计算出所需借用的最短时间。 - Sven Marnach
谢谢你的回答。我仍在与借用检查器作斗争。在我的实际程序中,get和set之间还有一层间接性:我获取上下文的名称并将其馈送到一个函数中,并根据函数输出更新foo。你提到的方法似乎都不适用于我:( 我想我把我的程序简化得太多了。 - xxks-kkk
1
我的观点是,如果你的代码使用原始指针引入可变别名,那就是你的代码的问题。编译器中没有错误,只有被输入到编译器中的代码存在错误。这段代码会错误地声称“此代码遵守了有效的Rust程序所需的保证,请相信我”。编译器不会引入任何东西,它信任按照指示提供的代码。 - Shepmaster
@SvenMarnach,所以可变指针别名并不是严格禁止的,而是当你将可变指针转换为引用时,传递到类似于fn(a: &mut T, b: &T)这样的函数中,其中ab是相同的地址空间。在编译函数时,它假定引用遵循正常的Rust规则,这意味着没有别名。基本上,如果你正在处理接受指针的函数,或者在同一代码中使用指针,那么是允许的,但是一旦开始转换回引用(你需要为了大量的Rust),你就很容易犯错。 - Linear
@LinearZoetrope 显然你需要遵守Rust的引用规则,但是你可以拥有指向同一内存的多个*mut指针。这难道不也是“可变指针别名”吗? - Sven Marnach
显示剩余14条评论

4

可能有一些答案已经可以回答您的问题,但是有很多情况会触发这个错误信息,所以我会回答您特定的情况。

更容易的解决方法是使用#![feature(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
        _ => (),
    }
}

1
我不确定除非你已经在夜间工作,否则我会引入特性门。另外,仅仅是我的个人意见,由于 _ => (), 这种情况,这确实更像是一个 if 的情况而不是 match,虽然这只是一种偏好。 - Linear
@LinearZoetrope 嗯,那真的是基于观点的,需要夜间版本,但并不“不安全”。我只是说这是最简单的解决方案(也是为了说明代码本身没问题,只是编译器没有nll不允许它)。如果...只需要一个测试,那么if比match更快。对于一个问题,总有很多解决方案。我没有声称列出所有解决方案,主要是因为我不知道所有解决方案 ;) - Stargateur
@LinearZoetrope 不需要功能门,只需使用Beta和2018版。NLL在那里默认开启,并将成为12月和2018版的默认选项。 - Shepmaster

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 变量是分开的时候,它的值可以在没有显式变异的情况下突然改变。


@LinearZoetrope 比我快了一分钟。看起来是一个更好的答案,不过。 - Jarak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接