【问题标题】:How do I implement an iterator from a vector of std::Rc<std::RefCell<T>> smart pointers?如何从 std::Rc<std::RefCell<T>> 智能指针的向量实现迭代器?
【发布时间】:2021-06-09 07:59:54
【问题描述】:

我正在尝试了解如何使用内部可变性。这个问题与my previous question密切相关。

我有一个通用结构Port&lt;T&gt;,它拥有一个Vec&lt;T&gt;。我们可以将端口 B 链接到端口 A,因此,在读取端口 A 的内容时,我们能够读取端口 B 的内容。但是,这种链接对端口 A 的阅读器是隐藏的。这就是我实现iter(&amp;self) 方法的原因:

use std::rc::Rc;

pub struct Port<T> {
    values: Vec<T>,
    ports: Vec<Rc<Port<T>>>,
}

impl <T> Port<T> {
    pub fn new() -> Self {
        Self { values: vec![], ports: vec![] }
    }

    pub fn add_value(&mut self, value: T) {
        self.values.push(value);
    }

    pub fn is_empty(&self) -> bool {
        self.values.is_empty() && self.ports.is_empty()
    }

    pub fn chain_port(&mut self, port: Rc<Port<T>>) {
        if !port.is_empty() {
            self.ports.push(port)
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &T> {
        self.values.iter().chain(
            self.ports.iter()
                .flat_map(|p| Box::new(p.iter()) as Box<dyn Iterator<Item = &T>>)
        )
    }

    pub fn clear(&mut self) {
        self.values.clear();
        self.ports.clear();
    }
}

应用程序具有以下伪代码行为:

  • 创建端口
  • 循环:
    • 用值填充端口
    • 链端口
    • 迭代端口的值
    • 清除端口

main 函数应如下所示:

fn main() {
    let mut port_a = Rc::new(Port::new());
    let mut port_b = Rc::new(Port::new());

    loop {
        port_a.add_value(1);
        port_b.add_value(2);

        port_a.chain_port(port_b.clone());

        for val in port_a.iter() {
            // read data
        };

        port_a.clear();
        port_b.clear();
    }
}

但是,编译器抱怨:

error[E0596]: cannot borrow data in an `Rc` as mutable
  --> src/modeling/port.rs:46:9
   |
46 |         port_a.add_value(1);
   |         ^^^^^^ cannot borrow as mutable
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Port<i32>>`

我已经阅读了几篇文章等,看来我需要使用Rc&lt;RefCell&lt;Port&lt;T&gt;&gt;&gt; 才能改变端口。我改了Port&lt;T&gt;的实现:

use std::cell::RefCell;
use std::rc::Rc;

pub struct Port<T> {
    values: Vec<T>,
    ports: Vec<Rc<RefCell<Port<T>>>>,
}

impl<T> Port<T> {

    // snip

    pub fn chain_port(&mut self, port: Rc<RefCell<Port<T>>>) {
        if !port.borrow().is_empty() {
            self.ports.push(port)
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &T> {
        self.values.iter().chain(
            self.ports
                .iter()
                .flat_map(|p| Box::new(p.borrow().iter()) as Box<dyn Iterator<Item = &T>>),
        )
    }

    // snip
}

这也不编译:

error[E0515]: cannot return value referencing temporary value
  --> src/modeling/port.rs:35:31
   |
35 |                 .flat_map(|p| Box::new(p.borrow().iter()) as Box<dyn Iterator<Item = &T>>),
   |                               ^^^^^^^^^----------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                               |        |
   |                               |        temporary value created here
   |                               returns a value referencing data owned by the current function

我想我知道问题出在哪里:p.borrow() 返回对被链接的端口的引用。我们使用该引用来创建迭代器,但是一旦函数完成,引用就会超出范围并且迭代器不再有效。

我不知道如何处理这个问题。我设法实现了以下不安全的方法:

    pub fn iter(&self) -> impl Iterator<Item = &T> {
        self.values.iter().chain(self.ports.iter().flat_map(|p| {
            Box::new(unsafe { (&*p.as_ref().as_ptr()).iter() }) as Box<dyn Iterator<Item = &T>>
        }))
    }

虽然这可行,但它使用了不安全的代码,并且必须有一个安全的解决方法。

我设置了playground 以获取有关我的应用程序的更多详细信息。应用程序编译并输出预期的结果(但使用了不安全的代码)。

【问题讨论】:

  • 为什么 main 必须是这样的?你不能每次迭代都做一个new() port_a, port_b 并保存手动clean() 调用吗?如果不是,为什么?这已经解决了所有直接的所有权问题——因为它们是该公式所固有的。为了摆脱剩余的复杂性,也许将递归迭代器重写为迭代器。
  • 我们的想法是我们有两个独立运行的不同“实体”。每个实体都有一个Port&lt;T&gt;(或Rc&lt;RefCell&lt;Port&lt;T&gt;&gt;&gt;),包含port_b的实体可以向包含port_a的实体发送消息。当然,这是一个非常简单的案例。通常,您将拥有数十个实体,每个实体都有 2 或 3 个端口,端口如何“链接”也取决于用例。在每次迭代中创建/销毁端口是否有效?
  • 您已经通过在每次迭代后手动清理结构来完成大部分工作。跨迭代携带结构没有用,但对所有权推理来说是一个重大(在这种情况下是致命的)负担。

标签: rust ownership reference-counting refcell


【解决方案1】:

您不能修改Rc 后面的任何内容,这是正确的。虽然这可以通过RefCell 解决,但您不想走那条路。您可能会遇到需要强制执行特定的clean() 命令或类似情况的情况。

更重要的是:你的主从根本上是有缺陷的,在所有权方面。采取以下措施:

let mut port_a = Port::new();
let mut port_b = Port::new();

loop {
    // Creates an iummutable borrow of port_b with same lifetime as port_a!
    port_a.chain_port(port_b);

    // ...

    // A mutable borrow of port_b.
    // But the immutable borrow from above persists across iterations.
    port_b.clear();
    // Or, even if you do fancy shenanigans at least until this line.
    port_a.clear();
}

要克服这个问题,只需将端口生命周期限制为一次迭代。无论如何,您目前都手动清理它们,所以这已经是您在概念上所做的。

另外,我摆脱了递归迭代,只是为了进一步简化一些事情。

#[derive(Clone)]
pub struct Port<'a, T> {
    values: Vec<T>,
    ports: Vec<&'a Port<'a, T>>,
}

impl<'a, T> Port<'a, T> {
    pub fn new() -> Self {
        Self {
            values: vec![],
            ports: vec![],
        }
    }

    pub fn add_value(&mut self, value: T) {
        self.values.push(value);
    }

    pub fn is_empty(&self) -> bool {
        self.values.is_empty() && self.ports.is_empty()
    }

    pub fn chain_port(&mut self, port: &'a Port<T>) {
        if !port.is_empty() {
            self.ports.push(&port)
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &T> {
        let mut port_stack: Vec<&Port<T>> = vec![self];

        // Sensible estimate I guess.
        let mut values: Vec<&T> = Vec::with_capacity(self.values.len() * (self.ports.len() + 1));

        while let Some(port) = port_stack.pop() {
            values.append(&mut port.values.iter().collect());
            port_stack.extend(port.ports.iter());
        }

        values.into_iter()
    }
}

fn main() {
    loop {
        let mut port_a = Port::new();
        let mut port_b = Port::new();

        port_a.add_value(1);
        port_b.add_value(2);

        port_a.chain_port(&port_b);
 
        print!("values in port_a: [ ");
        for val in port_a.iter() {
            print!("{} ", val);
        }
        println!("]");
    }
}

【讨论】:

  • 我将按照您的建议编写我的应用程序的快速原型。让我们看看进展如何
  • 我实现了一个新版本的应用程序,将您的想法与我之前问题的答案混合在一起。但是,我对代码进行了重大更改(我实现了Entity 结构)。我应该将此作为替代答案发布还是应该编辑我的问题?我用我的新代码创建了一个new playground。我很感激你的意见。提前致谢!
  • * 实体有输入和输出端口。 * 输入端口归实体所有,并且永远存在 * 输出端口在每次迭代时创建/填充/链接 * 我没有严格遵循您的建议,因为您迭代并使用所有链接端口来创建端口的迭代器。这可能会降低性能,不是吗?
  • 我可以看看,如果链接有效。如果您还有其他问题,请打开另一个问题。如果您有较小的编辑,请编辑问题。如果您实际上遇到了一个完全不同的问题,我认为发布替代答案没有任何意义 - 它不会使未来的读者通过搜索来到这里。
  • 关于性能:这取决于。如果您有两个包含一百万个元素的向量,则第一个选项可能会更好。如果你有一百万个包含两个元素的向量,我可以看到我的表现更好。并不是说这么小的数量会有任何显着差异。
猜你喜欢
  • 2021-08-24
  • 2021-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多