简体   繁体   中英

How to achieve encapsulation of struct fields without borrowing the struct as a whole

My question has already been somewhat discussed here .

The problem is that I want to access multiple distinct fields of a struct in order to use them, but I don't want to work on the fields directly. Instead I'd like to encapsulate the access to them, to gain more flexibility.

I tried to achieve this by writing methods for this struct, but as you can see in the question mentioned above or my older question here this approach fails in Rust because the borrow checker will only allow you to borrow different parts of a struct if you do so directly, or if you use a method borrowing them together, since all the borrow checker knows from its signature is that self is borrowed and only one mutable reference to self can exist at any time.

Losing the flexibility to borrow different parts of a struct as mutable at the same time is of course not acceptable. Therefore I'd like to know whether there is any idiomatic way to do this in Rust.

My naive approach would be to write a macro instead of a function, performing (and encapsulating) the same functionality.

EDIT: Because Frxstrem suggested that the question which I linked to at the top may answer my question I want to make it clear, that I'm not searching for some way to solve this . My question is which of the proposed solutions (if any) is the right way to do it?

After some more research it seems that there is no beautiful way to achieve partial borrowing without accessing the struct fields directly, at least as of now.

There are, however, multiple imperfect solutions to this which I'd like to discuss a little, so that anyone who might end up here may weigh the pros and cons for themself. I'll use the code from my original question as an example to illustrate them here.

So let's say you have a struct:

struct GraphState {
    nodes: Vec<Node>,
    edges: Vec<Edge>,
}

and you're trying to do borrow one part of this struct mutably and the other immutably:

// THIS CODE DOESN'T PASS THE BORROW-CHECKER

impl GraphState {
    pub fn edge_at(&self, edge_index: u16) -> &Edge {
        &self.edges[usize::from(edge_index)]
    }

    pub fn node_at_mut(&mut self, node_index: u8) -> &mut Node {
        &mut self.nodes[usize::from(node_index)]
    }

    pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = self.edge_at(edge_index);    // first (immutable) borrow here
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                self.node_at_mut(node_index).remove_edge(edge_index);   // second (mutable)
                                                                        // borrow here -> ERROR
            }
        }
    }
}

But of course this fails, since you're not allowed to borrow self as mutable and immutable at the same time.

So to get around this you could simply access the fields directly:

impl GraphState {
    pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = &self.edges[usize::from(edge_index)];
        for i in 0..2 {
            let node_index = edge.node_indices[i];
            self.nodes[usize::from(node_index)].remove_edge(edge_index);
        }
    }
}

This approach works, but it has two major drawbacks:

  1. The accessed fields need to be public (at least if you want to allow access to them from another scope). If they're implementation details that you'd rather have private you're in bad luck.
  2. You always need to operate on the fields directly . This means that code like usize::from(node_index) needs to be repeated everywhere, which makes this approach both brittle and cumbersome.

So how can you solve this?

A) Borrow everything at once

Since borrowing self mutably multiple times is not allowed, mutably borrowing all the parts you want at once instead is one straightforward way to solve this problem:

pub fn edge_at(edges: &[Edge], edge_index: u16) -> &Edge {
    &edges[usize::from(edge_index)]
}
pub fn node_at_mut(nodes: &mut [Node], node_index: u8) -> &mut Node {
    &mut nodes[usize::from(node_index)]
}

impl GraphState {
    pub fn data_mut(&mut self) -> (&mut [Node], &mut [Edge]) {
        (&mut self.nodes, &mut self.edges)
    }

    pub fn remove_edge(&mut self, edge_index: u16) {
        let (nodes, edges) = self.data_mut();    // first (mutable) borrow here
        let edge = edge_at(edges, edge_index);    
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                node_at_mut(nodes, node_index).remove_edge(edge_index);   // no borrow here
                                                                          // -> no error
            }
        }
    }
}

It's clearly a workaround and far from ideal, but it works and it allows you to keep the fields themselves private (though you'll probably need to expose the implementation somewhat, as the user has to hand the necessary data to the other functions by hand).

B) Use a macro

If code reuse is all you worry about and visibility is not an issue to you you can write macros like these:

macro_rules! node_at_mut {
    ($this:ident, $index:expr) => {
        &mut self.nodes[usize::from($index)]
    }
}
macro_rules! edge_at {
    ($this:ident, $index:expr) => {
        &mut self.edges[usize::from($index)]
    }
}
...
pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = edge_at!(self, edge_index);
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                node_at_mut!(self, node_index).remove_edge(edge_index);
        }
    }
}

If your fields are public anyway I'd probably go with this solution, as it seems the most elegant to me.

C) Go unsafe

What we want to do here is obviously safe. Sadly the borrow checker cannot see this, since the function signatures tell him no more than that self is being borrowed. Luckily Rust allows us to use the unsafe keyword for cases like these:

pub fn remove_edge(&mut self, edge_index: u16) {
    let edge: *const Edge = self.edge_at(edge_index);
    for i in 0..2 {
        unsafe {
            let node_index = (*edge).node_indices[i];
            self.node_at_mut(node_index).remove_edge(edge_index);   // first borrow here
                                                                    // (safe though since remove_edge will not invalidate the first pointer)
        }
    }
}

This works and gives us all the benefits of solution A) , but using unsafe for something that could easily be done without, if only the language had some syntax for actual partial borrowing , seems a bit ugly to me. On the other hand it may (in some cases) be preferable to solution A), due to its sheer clunkyness...

EDIT: After some thought I realised, that we know this approach to be safe here, only because we know about the implementation . In a different use case the data in question might actually be contained in one single field (say a Map), even if it looks like we are accessing two very distinct kinds of data when calling from outside. This is why this last approach is rightfully unsafe, as there is no way for the borrow checker to check whether we are actually borrowing different things without exposing the private fields, rendering our effort pointless .

Contrary to my initial belief, this couldn't even really be changed by expanding the language. The reason is that one would still need to expose information about private fields in some way for this to work .

After writing this answer I also found this blog post which goes a bit deeper into possible solutions (and also mentions some advanced techniques that didn't come to my mind, none of which is universally applicable though). If you happen to know another solution, or a way to improve the ones proposed here, please let me know.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM