Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
192 views
in Technique[技术] by (71.8m points)

rust - Why does refactoring by extracting a method trigger a borrow checker error?

The architecture of my networking application can be stripped down to the following:

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

This works OK. I would like to make a separate method in UserHandler that handles user input when we have already established that user with given id exists. So it becomes:

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

All of a sudden, it doesn't compile!

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

At first glance, the error is pretty obvious: you can not have a mutable reference to self and to an attribute of self - it is like having two mutable references to self. But then, what the heck, I do have two mutable references like this in the original code!

  1. Why does this simple refactoring trigger borrow checker error?
  2. How do I work around it and decompose UserHandler::handle_data method like this?

If you wonder why I want such refactoring, consider a case when there are multiple types of messages that user could send, all need to be handled differently, but there is a common part: having to know which User object sent this message.

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The compiler is right to prevent you from borrowing the HashMap twice. Suppose in handle_user_data() you also tried to borrow self.users. You would break the rules of borrowing in Rust because you already have a mutable borrow on it and you can only have one.

Since you can't borrow self twice for your handle_user_data(), I will propose a solution. I don't know if it's the best, but it works without unsafe and without overhead (I think).

The idea is to use an intermediate struct that will borrow the other fields of 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;
    }
}

This way, the compiler knows we can't borrow users twice.

If you have only one or two things to borrow, a quick solution is to have an associated function that takes them as parameters:

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

We can improve this design:

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

Now we are sure we don't have overhead and the syntax is much cleaner.

Further information can be found in Niko Matsakis' blog post After NLL: Interprocedural conflicts. Mapping this answer to the blog post:

  • solution #1 -> "View structs as a general, but extreme solution" section
  • solution #2 -> "Free variables as a general, but extreme solution" section (here expressed as an associated function)
  • solution #3 -> "Factoring as a possible fix" section

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...