如何将引用的生命周期移到 Rust 的范围之外

How to move the lifetime of references outside a scope in Rust

实际上我尝试在 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 循环之外使用分配的结构。我的意思是在循环内分配结构并稍后在循环外使用它是一个常见的用例。

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

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

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

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

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

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

这是弱引用的用武之地,它允许您引用由 Rc 维护的另一个值,但不会阻止它被收集。但是,如果存在两个或多个对同一值的引用,则无法更改 Rc 中的值,因此将弱引用添加到节点需要通过 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);
        }
    }
}

节点是 strongly-referenced by the_nodes_nodes,这将使它们保持活动状态,但我们可以分发更多 RcWeak 引用同一节点的实例,而不几乎需要严格管理生命周期。

请注意,当 Node 由于从地图中删除而被销毁时,对该节点的现有 Weak 引用将不再有效。您必须在 Weak 引用上调用 upgrade(),只有当 Node 值仍然存在时才会返回 Rcget_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 用于获取对节点的可变引用。