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

标签 rust borrow-checker

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

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

这工作正常。当我们已经确定具有给定 ID 的用户存在时,我想在 UserHandler 中创建一个单独的方法来处理用户输入。于是就变成了:

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

Playground

突然之间,它无法编译!

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

乍一看,错误非常明显:您不能对 selfself 的属性有可变引用 - 这就像有两个可变引用到 self 。但是,到底是什么,我确实在原始代码中有两个像这样的可变引用!

  1. 为什么这个简单的重构会触发借用检查器错误?
  2. 我该如何解决它并像这样分解 UserHandler::handle_data 方法?

如果你想知道我为什么要进行这样的重构,请考虑这样一种情况:用户可以发送多种类型的消息,所有类型的消息都需要以不同的方式处理,但有一个共同的部分:必须知道哪个 User 对象发送了这条消息。

最佳答案

编译器会阻止你两次借用HashMap。假设在 handle_user_data() 中您还尝试借用 self.users。你会违反 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 -> “作为可能的解决方案的因式分解”部分

关于rust - 为什么通过提取方法进行重构会触发借用检查器错误?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57058469/

相关文章:

rust - 无法从 &mut self 借用文件(错误消息 : cannot move out of borrowed content)

c# - 将大量数据从 Rust FFI 库返回给 C# 调用者的最快方法是什么?

rust - 构造类型的向量并初始化它们

math - 正弦函数在哪里?

process - 无法通过管道多次传入或传出生成的子进程

vector - 如何从另一个矢量制作盒装特征对象的矢量?

rust - 就地更新 Rust 枚举很尴尬

rust - 如何在不将可变引用传递给函数的情况下重新借用它?

multithreading - 使用Mutex防止同时处理同一类型时进行多线程列表迭代

python - 如何实现rust-cpython as_object()和into_object()?