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

这样就可以了。我想在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;
    }
}

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进行了两个可变的引用一样。但是,到底,我在原始代码中有两个这样的可变引用!
  • 为什么这个简单的重构会触发借阅检查器错误?
  • 如何解决该问题并分解UserHandler::handle_data这样的方法?

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

    最佳答案

    编译器是正确的,可以防止您两次借用HashMap。假设您在handle_user_data()中还尝试借用self.users。您将破坏Rust中的借用规则,因为您已经在其上进行了可变借用,并且只能拥有一个借用。

    由于您不能两次为自己的self借用handle_user_data(),因此我将提出一个解决方案。我不知道这是否是最好的,但是它可以在不安全和没有开销的情况下工作(我认为)。

    这个想法是使用一个中间结构,该结构将借用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/61845186/

    相关文章:

    rust - 本地拥有的引用资料被认为是借来的

    Rust 零复制生命周期处理

    rust - Rust中不同整数类型的函数参数

    rust - 在 C 回调中捕获变量

    rust - 如何在不退出程序/进程的情况下关闭由 winit 启动的窗口?

    rust - 检查 Rust 中所有向量的长度是否相同

    rust - 为什么我得到 "no method named ` join` found for type `[usize]` in the current scope”?

    Rust:在无限循环中借用检查器

    json - 尝试从标准输入读取 JSON 时出现借用错误

    rust - 与可变借用相关的不可变借用导致 "cannot borrow ` *self` 一次多次可变”