0

实际上,我尝试在 Rust 中实现以下功能。

我想要一个结构节点,它有一个指向其他节点结构的向量。此外,我有一个主向量,它保留所有已实例化的节点结构。

这里的关键点是节点是在循环内分配的(即自己的范围),并且保持所有结构(或对结构的引用)的主向量在循环外声明,在我看来这是一个 0815 用例。

经过大量尝试,我想出了仍然无法编译的代码。实际上我只用 &Node 和 RefCell<&Node> 尝试过,两者都不能编译。

struct Node<'a> {
    name: String,
    nodes: RefCell<Vec<&'a Node<'a>>>,
}

impl<'a> Node<'a> {

    fn create(name: String) -> Node<'a> {
        Node {
            name: name,
            nodes: RefCell::new(Vec::new()),
        }
    }
    
    fn add(&self, value: &'a Node<'a>) {
        self.nodes.borrow_mut().push(value);
    }

    fn get_nodes(&self) -> Vec<&'a Node> {
        self.nodes.take()
    }
}


// Later the code ...

    let mut the_nodes_ref: HashMap<String, RefCell<&Node>> = HashMap::new();
    let mut the_nodes_nodes: HashMap<String, &Node> = HashMap::new();

    // This works
    let no1_out = Node::create(String::from("no1"));
    let no2_out = Node::create(String::from("no2"));

    no1_out.add(&no2_out);
    no2_out.add(&no1_out);

    the_nodes_nodes.insert(no1_out.name.clone(), &no1_out);
    the_nodes_nodes.insert(no2_out.name.clone(), &no2_out);

    let no1_ref_out = RefCell::new(&no1_out);
    let no2_ref_out = RefCell::new(&no2_out);

    the_nodes_ref.insert(no1_out.name.clone(), no1_ref_out);
    the_nodes_ref.insert(no2_out.name.clone(), no2_ref_out);

    // This works not because no1 and no2 do not live long enough
    let items = [1, 2, 3];
    for _ in items {
        let no1 = Node::create(String::from("no1"));
        let no2 = Node::create(String::from("no2"));

        no1.add(&no2); // <- Error no2 lives not long enough
        no2.add(&no1); // <- Error no1 lives not long enough

        the_nodes_nodes.insert(no1.name.clone(), &no1);
        the_nodes_nodes.insert(no2.name.clone(), &no2);

        let no1_ref = RefCell::new(&no1);
        let no2_ref = RefCell::new(&no2);

        the_nodes_ref.insert(no1.name.clone(), no1_ref);
        the_nodes_ref.insert(no2.name.clone(), no2_ref);
    }

我有点理解这个问题,但我想知道如何解决这个问题。如何在单独的范围内分配结构(此处为 for 循环),然后在 for 循环之外使用分配的结构。我的意思是在循环内分配结构并稍后在循环外使用它是一个常见的用例。

不知何故,我觉得缺少的链接是通过生命周期参数告诉 Rust 编译器,引用也应该在 for 循环之外保持活动状态,但我不知道该怎么做。但也许这也不是正确的方法......

实际上,这里的另一个关键点是我希望节点具有对其他节点的引用,而不是节点的副本。主向量也是如此,该向量应该具有对分配节点的引用,而不是节点的副本。

4

1 回答 1

2

所有这一切都归结为一个问题的答案:程序中的哪个实体应该拥有这些Node值?

现在main()拥有这些值,并且您知道这一点,因为程序中的其他所有内容都只有&Node,这是对其他事物拥有的事物的引用。这就是循环变体失败的原因,因为no1no2 拥有的值,但它们在每次循环迭代结束时被销毁,因此您的地图中有悬空引用。

解决这个问题的一种方法是让一个集合拥有这些值。但是,由于 Rust 的借用规则,一旦您开始提供引用,您将无法修改集合,因为这需要可变地借用集合。因此,您必须预先创建所有节点,将它们放入集合中,然后开始向其他节点提供引用。这是解决问题的最有效方法,但不灵活,并且将所有节点的生命周期绑定在一起。在实际代码中,节点可能会来来去去,因此让它们共享生命周期是不切实际的。

这个问题的经典解决方案是通过 共享所有权Rc,但这会带来一系列问题,即节点相互引用。在这种情况下,即使从全局集合中删除节点对象,您也可能会泄漏节点对象,因为它们仍然相互引用。

这就是弱引用的用武之地,它允许您引用由 an 维护的另一个值,Rc但不会阻止它被收集。但是,Rc如果存在两个或多个对同一个值的引用,则不能改变 an 中的值,因此将弱引用添加到节点需要通过 进行内部可变性RefCell

让我们把所有这些放在一起:

use std::collections::HashMap;
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    name: String,
    nodes: RefCell<Vec<Weak<Node>>>,
}

impl Node {
    fn new(name: String) -> Self {
        Node { name, nodes: RefCell::new(Vec::new()) }
    }
    
    fn name(&self) -> &String {
        &self.name
    }
    
    fn add(&self, value: Weak<Node>) {
        self.nodes.borrow_mut().push(value);
    }

    fn get_nodes(&self) -> Vec<Rc<Node>> {
        // Return strong references.  While we are doing this, clean out
        // any dead weak references.
        let mut strong_nodes = Vec::new();
        
        self.nodes.borrow_mut().retain(|w| match w.upgrade() {
            Some(v) => {
                strong_nodes.push(v);
                true
            },
            None => false,
        });
        
        strong_nodes
    }
}

fn main() {
    let mut the_nodes_nodes: HashMap<String, Rc<Node>> = HashMap::new();

    let items = [1, 2, 3];
    for _ in items {
        let no1 = Rc::new(Node::new(String::from("no1")));
        let no2 = Rc::new(Node::new(String::from("no2")));
        
        // downgrade creates a new Weak<T> for an Rc<T>
        no1.add(Rc::downgrade(&no2));
        no2.add(Rc::downgrade(&no1));
        
        for n in [no1, no2] {
            the_nodes_nodes.insert(n.name().clone(), n);
        }
    }
}

节点被 强烈引用the_nodes_nodes,这将使它们保持活动状态,但我们可以进一步分配引用同一节点RcWeak实例,而无需几乎同样严格地管理生命周期。

请注意,当 aNode因为从地图中删除而被销毁时Weak,对该节点的现有引用将不再有效。您必须调用引用,upgrade()仅当值仍然存在时才会返回。该方法通过返回一个仅强烈引用仍然活动的节点的方法来包装此逻辑。WeakRcNodeget_nodes()IntoIterator


为了完整起见,这里是非Rc选项的样子。有一个辅助结构Nodes来保存地图。

use std::collections::HashMap;
use std::cell::RefCell;

struct Node<'a> {
    name: String,
    nodes: RefCell<Vec<&'a Node<'a>>>,
}

impl<'a> Node<'a> {
    fn new(name: String) -> Self {
        Node { name, nodes: RefCell::new(Vec::new()) }
    }
    
    fn name(&self) -> &String {
        &self.name
    }
    
    fn add(&self, value: &'a Node<'a>) {
        self.nodes.borrow_mut().push(value);
    }
    
    fn get_nodes(&self) -> Vec<&'a Node<'a>> {
        self.nodes.borrow().clone()
    }
}

struct Nodes<'a> {
    nodes: HashMap<String, Node<'a>>,
}

impl<'a> Nodes<'a> {
    fn new<T: IntoIterator<Item=String>>(node_names: T) -> Self {
        let mut nodes = HashMap::new();
        
        for name in node_names {
            nodes.insert(name.clone(), Node::new(name));
        }
        
        Self { nodes }
    }
    
    fn get_node(&'a self, name: &String) -> Option<&'a Node<'a>> {
        self.nodes.get(name)
    }
}

fn main() {
    let nodes = Nodes::new(["n1".to_string(), "n2".to_string()]);
    
    let n1 = nodes.get_node(&"n1".to_string()).expect("n1");
    let n2 = nodes.get_node(&"n2".to_string()).expect("n2");
    
    n1.add(n2);
    n2.add(n1);
}

请注意,我们必须提前创建所有节点。创建节点需要借用HashMap可变的,当映射中有对值的引用时,我们不能这样做。该Nodes类型通过要求在其构造函数中创建节点名称的迭代器来明确这一点;API 不允许稍后添加新节点。

当我们持有对任何其他节点的引用时,我们无法获得对节点的可变引用,因此这种方法还需要RefCell每个节点的节点列表具有内部可变性 (),并且根本不提供用于获取对节点的可变引用的 API。

于 2022-02-20T15:51:02.993 回答