【问题标题】:How to achieve encapsulation of struct fields without borrowing the struct as a whole如何在不整体借用struct的情况下实现对struct字段的封装
【发布时间】:2021-04-03 14:59:13
【问题描述】:

我的问题已经有点discussed here了。

问题是我想访问结构的多个不同字段以使用它们,但我不想直接处理这些字段。相反,我想封装对它们的访问,以获得更大的灵活性。

我试图通过为此结构编写方法来实现这一点,但正如您在上面提到的问题或my older question here 中看到的那样,这种方法在 Rust 中失败了,因为借用检查器只允许您借用结构的不同部分,如果您可以直接这样做,或者如果您使用一种方法一起借用它们,因为借用检查器从其签名中知道的所有内容是 self 是借用的,并且任何时候都只能存在一个对 self 的可变引用。

失去将结构的不同部分同时借用为可变部分的灵活性当然是不可接受的。因此,我想知道在 Rust 中是否有任何惯用的方法来做到这一点。

我的幼稚方法是编写宏而不是函数,执行(和封装)相同的功能。

编辑: 因为 Frxstrem 建议我在顶部链接到的问题可能会回答我的问题,所以我想澄清一下,我不是在寻找 某种解决此问题的方法。我的问题是,哪些建议的解决方案(如果有)是正确的方法?

【问题讨论】:

  • 不,但它为如何做到这一点提供了一些可能性。如果我有足够的声誉,我会在那里评论建议使用宏的答案,但遗憾的是我没有,所以我不得不问我自己的问题。

标签: rust idioms borrow-checker


【解决方案1】:

经过更多研究,似乎没有漂亮的方法可以在不直接访问结构字段的情况下实现部分借用,至少目前是这样。

不过,我想稍微讨论一下这个问题的多种不完美的解决方案,以便任何可能最终来到这里的人都可以自己权衡利弊。 我将在这里以the code from my original question 为例进行说明。

假设你有一个结构:

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

并且您正试图以可变方式借用该结构的一部分,而另一部分则不可变地借用:

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

但这当然失败了,因为你不能同时借用 self 作为可变和不可变的。

所以要解决这个问题,您可以直接访问这些字段:

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

这种方法有效,但它有两个主要缺点:

  1. 访问的字段需要是公共的(至少如果您想允许从另一个范围访问它们)。如果它们是您希望私有的实现细节,那您就倒霉了。
  2. 您始终需要直接对字段进行操作。这意味着像usize::from(node_index) 这样的代码需要到处重复,这使得这种方法既脆弱又麻烦。

那么你怎么解决这个问题呢?

A) 一次性借用所有东西

由于不允许多次可变借用self,因此一次可变借用您想要的所有部分是解决此问题的一种直接方法:

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

这显然是一种解决方法,远非理想,但它有效,并且允许您将字段本身保密(尽管您可能需要在一定程度上公开实现,因为用户必须将必要的数据交给其他人手动函数)。

B) 使用宏

如果您只担心代码重用并且可见性对您来说不是问题,您可以编写如下宏:

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

如果你的字段是公开的,我可能会选择这个解决方案,因为它对我来说似乎是最优雅的。

C) 变得不安全

我们在这里要做的显然是安全的。 遗憾的是借用检查器看不到这一点,因为函数签名告诉他的只是self 正在被借用。幸运的是,Rust 允许我们在以下情况下使用 unsafe 关键字:

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

这行得通并为我们提供了解决方案 A) 的所有好处,但使用 unsafe 可以轻松完成的事情 如果只有语言有一些用于实际部分借用的语法 /s>,对我来说似乎有点难看。另一方面,它可能(在某些情况下)比解决方案 A)更可取,因为它非常笨重......

编辑:经过一番思考,我意识到,我们知道这种方法在这里是安全的,只是因为我们知道实施。在不同的用例中,有问题的数据实际上可能包含在一个字段中(例如 Map),即使看起来我们在从外部调用时访问的是两种截然不同的数据。 这就是为什么最后一种方法是不安全的,因为借用检查器无法在不暴露私有字段的情况下检查我们是否真的借用了不同的东西,从而使我们的努力毫无意义

与我最初的想法相反,这甚至无法通过扩展语言来真正改变。 The reason is that one would still need to expose information about private fields in some way for this to work.

写完这个答案后,我还发现了this blog post,它更深入地探讨了可能的解决方案(还提到了一些我没有想到的先进技术,但这些技术都不是普遍适用的)。如果您碰巧知道其他解决方案或改进此处提出的解决方案的方法,请告诉我。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 2020-07-19
    • 1970-01-01
    • 1970-01-01
    • 2020-04-04
    相关资源
    最近更新 更多