为什么通过提取方法进行重构会触发借用检查器错误?

10

我的网络应用程序的架构可以简化为以下内容:

use std::collections::HashMap;

/// Represents remote user. Usually has fields,
/// but we omit them for the sake of example.
struct User;

impl User {
    /// Send data to remote user.
    fn send(&mut self, data: &str) {
        println!("Sending data to user: \"{}\"", data);
    }
}

/// A service that handles user data.
/// Usually has non-trivial internal state, but we omit it here.
struct UserHandler {
    users: HashMap<i32, User>,  // Maps user id to User objects.
    counter: i32  // Represents internal state
}

impl UserHandler {
    fn handle_data(&mut self, user_id: i32, data: &str) {
        if let Some(user) = self.users.get_mut(&user_id) {
            user.send("Message received!");
            self.counter += 1;
        }
    }
}

fn main() {
    // Initialize UserHandler:
    let mut users = HashMap::new();
    users.insert(1, User{});
    let mut handler = UserHandler{users, counter: 0};

    // Pretend we got message from network:
    let user_id = 1;
    let user_message = "Hello, world!";
    handler.handle_data(user_id, &user_message);
}

Playground

这个可以正常工作。我想在 UserHandler 中创建一个单独的方法,用于处理已经确认存在具有给定 id 的用户输入。因此它变成了:

impl UserHandler {
    fn handle_data(&mut self, user_id: i32, data: &str) {
        if let Some(user) = self.users.get_mut(&user_id) {
            self.handle_user_data(user, data);
        }
    }

    fn handle_user_data(&mut self, user: &mut User, data: &str) {
        user.send("Message received!");
        self.counter += 1;
    }
}

游乐场

突然间,它不能编译了!

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:24:13
   |
23 |         if let Some(user) = self.users.get_mut(&user_id) {
   |                             ---------- first mutable borrow occurs here
24 |             self.handle_user_data(user, data);
   |             ^^^^                  ---- first borrow later used here
   |             |
   |             second mutable borrow occurs here
乍一看,错误非常明显:您不能同时拥有对 `self` 可变引用和其属性的可变引用——这就像同时拥有两个对 `self` 的可变引用。但是,在原始代码中,我确实像这样拥有了两个可变引用!
  1. 为什么这个简单的重构会触发借用检查器错误?
  2. 如何解决这个问题并将 `UserHandler::handle_data` 方法分解成以下形式?

如果你想知道我为什么想要这样的重构,请考虑这样一个情况:用户可能发送多种类型的消息,需要分别处理,但有一个公共部分:需要知道发送此消息的 `User` 对象。


谢谢。不过,我必须借用它,并且作为可变的借用,因为真正的代码在消息处理程序中修改了UserHandler状态。 - kreo
修改了问题以反映这一点。 - kreo
2个回答

13
编译器阻止你两次借用 HashMap 是正确的。假设在 handle_user_data() 中,你也试图借用 self.users。因为你已经对它进行了可变借用,而 Rust 规定一次只能有一个可变借用,所以这将违反 Rust 的借用规则。
由于你不能为 handle_user_data() 两次借用 self,我会提出一个解决方案。我不知道这是否是最好的方法,但它可以在不使用不安全和不增加开销的情况下工作(我认为)。
这个想法是使用一个中间结构体来借用 self 的其他字段:
impl UserHandler {
    fn handle_data(&mut self, user_id: i32, data: &str) {
        if let Some(user) = self.users.get_mut(&user_id) {
            Middle::new(&mut self.counter).handle_user_data(user, data);
        }
    }
}

struct Middle<'a> {
    counter: &'a mut i32,
}

impl<'a> Middle<'a> {
    fn new(counter: &'a mut i32) -> Self {
        Self {
            counter
        }
    }

    fn handle_user_data(&mut self, user: &mut User, data: &str) {
        user.send("Message received!");
        *self.counter += 1;
    }
}

这样,编译器就知道我们不能两次借用 users

如果你只需要借用一个或两个变量,一个快速的解决方案是创建一个关联函数,并将它们作为参数传递:

impl UserHandler {
    fn handle_user_data(user: &mut User, data: &str, counter: &mut i32) {
        // ...
    }
}

我们可以改进这个设计:

struct UserHandler {
    users: HashMap<i32, User>, // Maps user id to User objects.
    middle: Middle,              // Represents internal state
}

impl UserHandler {
    fn handle_data(&mut self, user_id: i32, data: &str) {
        if let Some(user) = self.users.get_mut(&user_id) {
            self.middle.handle_user_data(user, data);
        }
    }
}

struct Middle {
    counter: i32,
}

impl Middle {
    fn new(counter: i32) -> Self {
        Self {
            counter
        }
    }

    fn handle_user_data(&mut self, user: &mut User, data: &str) {
        user.send("Message received!");
        self.counter += 1;
    }
}

现在我们确定我们没有开销,并且语法更加简洁。

更多信息可以在Niko Matsakis的博客文章After NLL: Interprocedural conflicts中找到。将此答案映射到博客文章:

  • 解决方案#1 ->“将结构体视为一般但极端的解决方案”部分
  • 解决方案#2 ->“将自由变量视为一般但极端的解决方案”部分(此处表示为关联函数)
  • 解决方案#3 ->“作为可能修复的因素”部分

或者基本上有一个方法 fn handle_user_date(user: &mut User, data: &str, counter: &mut i32) - Gurwinder Singh
1
@GurwinderSingh 是的,但我试图解释一般的方法,因为如果有42个字段需要借用,那将会很痛苦。 - Stargateur
谢谢,你对错误原因的解释很有启发性。基本上,借用检查器可以防止一整类错误,比如在处理程序方法中插入到self.users中,这会使User引用无效! - kreo

4
当调用self.handle_user_data时,你正在可变地获取整个self,同时仍然拥有一个可变借用的user对象,这是借用检查器所不喜欢的。你不能同时拥有两个可变的借用。
解决这个问题的一种方法是不可变地获取self,而是可变地获取counter
impl UserHandler {
    fn handle_data(&mut self, user_id: i32, data: &str) {
        if let Some(user) = self.users.get_mut(&user_id) {
            handle_user_data(user, data, &mut self.counter);
        }
    }
}

fn handle_user_data(user: &mut User, data: &str, counter: &mut i32) {
    user.send(data);
    *counter += 1;
}

喜欢这个想法。 - McGrady

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