【问题标题】:Returning iterator of a Vec in a RefCell在 RefCell 中返回 Vec 的迭代器
【发布时间】:2016-02-06 02:33:37
【问题描述】:

鉴于以下structimpl

use std::slice::Iter;
use std::cell::RefCell;

struct Foo {
    bar: RefCell<Vec<u32>>,
}

impl Foo {
    pub fn iter(&self) -> Iter<u32> {
        self.bar.borrow().iter()
    }
}

fn main() {}

我收到一条关于终身问题的错误消息:

error: borrowed value does not live long enough
  --> src/main.rs:9:9
   |
9  |         self.bar.borrow().iter()
   |         ^^^^^^^^^^^^^^^^^ does not live long enough
10 |     }
   |     - temporary value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the body at 8:36...
  --> src/main.rs:8:37
   |
8  |       pub fn iter(&self) -> Iter<u32> {
   |  _____________________________________^ starting here...
9  | |         self.bar.borrow().iter()
10 | |     }
   | |_____^ ...ending here

我如何才能返回和使用bars 迭代器?

【问题讨论】:

    标签: iterator rust lifetime


    【解决方案1】:

    您不能这样做,因为它可以让您绕过运行时检查唯一性违规。

    RefCell 为您提供了一种将可变性排他性检查“推迟”到运行时的方法,作为交换,它允许通过共享引用对其内部保存的数据进行突变。这是使用 RAII 守卫完成的:您可以使用对RefCell 的共享引用获取守卫对象,然后使用此守卫对象访问RefCell 内的数据:

    &'a RefCell<T>        -> Ref<'a, T> (with borrow) or RefMut<'a, T> (with borrow_mut)
    &'b Ref<'a, T>        -> &'b T
    &'b mut RefMut<'a, T> -> &'b mut T
    

    这里的关键点是'b'a 不同,后者允许在没有&amp;mutRefCell 的引用的情况下获得&amp;mut T 引用。但是,这些引用将链接到守卫,并且不能比守卫活得更长。这是有意完成的:RefRefMut 析构函数在其 RefCell 内切换各种标志以强制进行可变性检查,并在这些检查失败时强制 borrow()borrow_mut() 恐慌。

    您可以做的最简单的事情是返回一个围绕Ref 的包装器,一个将实现IntoIterator 的引用:

    use std::cell::Ref;
    
    struct VecRefWrapper<'a, T: 'a> {
        r: Ref<'a, Vec<T>>
    }
    
    impl<'a, 'b: 'a, T: 'a> IntoIterator for &'b VecRefWrapper<'a, T> {
        type IntoIter = Iter<'a, T>;
        type Item = &'a T;
    
        fn into_iter(self) -> Iter<'a, T> {
            self.r.iter()
        }
    }
    

    (试试看on playground

    您不能直接为VecRefWrapper 实现IntoIterator,因为这样内部的Ref 将被into_iter() 使用,这与您现在所处的情况基本相同。

    【讨论】:

    • 一生太难了!恭喜你理解并解释它。
    【解决方案2】:

    替代解决方案

    这是一个替代解决方案,它按预期使用内部可变性。我们应该为Ref&lt;T&gt; 值创建一个迭代器,而不是为&amp;T 值创建一个迭代器,它会自动遵循。

    struct Iter<'a, T> {
        inner: Option<Ref<'a, [T]>>,
    }
    
    impl<'a, T> Iterator for Iter<'a, T> {
        type Item = Ref<'a, T>;
    
        fn next(&mut self) -> Option<Self::Item> {
            match self.inner.take() {
                Some(borrow) => match *borrow {
                    [] => None,
                    [_, ..] => {
                        let (head, tail) = Ref::map_split(borrow, |slice| {
                            (&slice[0], &slice[1..])
                        });
                        self.inner.replace(tail);
                        Some(head)
                    }
                },
                None => None,
            }
        }
    }
    

    Playground

    说明

    公认的答案有一些明显的缺点,可能会使 Rust 新手感到困惑。我将解释,根据我的个人经验,公认的答案实际上可能对初学者有害,以及为什么我相信这种替代方案使用了内部可变性和迭代器。

    正如前面的答案所强调的那样,使用RefCell 创建了一个不同的类型层次结构,它将对共享值的可变和不可变访问隔离开来,但您不必担心解决迭代的生命周期问题:

      RefCell<T>       .borrow()  ->     Ref<T>      .deref()  ->       &T
      RefCell<T>   .borrow_mut()  ->  RefMut<T>  .deref_mut()  ->   &mut T
    

    在没有生命周期的情况下解决此问题的关键是 Ref::map 方法,book 中严重遗漏了该方法。 Ref::map“对借用数据的组件进行新的引用”,或者换句话说,将外部类型的 Ref&lt;T&gt; 转换为某个内部值的 Ref&lt;U&gt;

    Ref::map(Ref<T>, ...)  ->  Ref<U>
    

    Ref::map 和它的对应物 RefMut::map 是内部可变性模式的真正明星,不是 borrow()borrow_mut()

    为什么?因为与borrow()borrow_mut() 不同,Ref::mutRefMut::map 允许您创建对可以“返回”的内部值的引用。

    考虑将first() 方法添加到问题中描述的Foo 结构:

    fn first(&self) -> &u32 {
          &self.bar.borrow()[0]
    }
    

    不,.borrow() 会生成一个临时的 Ref,它只会在方法返回之前存在:

    error[E0515]: cannot return value referencing temporary value
     --> src/main.rs:9:11
      |
    9 |           &self.bar.borrow()[0]
      |           ^-----------------^^^
      |           ||
      |           |temporary value created here
      |           returns a value referencing data owned by the current function
    
    error: aborting due to previous error; 1 warning emitted
    

    如果我们将其分解并使隐含的尊重显式化,我们可以使正在发生的事情变得更加明显:

    fn first(&self) -> &u32 {
        let borrow: Ref<_> = self.bar.borrow();
        let bar: &Vec<u32> = borrow.deref();
        &bar[0]
    }
    

    现在我们可以看到.borrow() 创建了一个Ref&lt;T&gt;,该Ref&lt;T&gt; 归方法的范围所有,并且没有返回,因此甚至在它提供的引用可以使用之前就被删除了。所以,我们真正需要的是返回一个拥有的类型而不是一个引用。我们想返回一个Ref&lt;T&gt;,因为它为我们实现了Deref

    Ref::map 将帮助我们为组件(内部)值做到这一点:

    fn first(&self) -> Ref<u32> {
        Ref::map(self.bar.borrow(), |bar| &bar[0])
    }
    

    当然,.deref() 仍然会自动发生,而Ref&lt;u32&gt; 将主要是引用透明的 &amp;u32

    知道了。 使用Ref::map 时容易犯的一个错误是尝试在闭包中创建一个拥有的值,这在我们尝试使用borrow() 时是不可能的。考虑第二个参数的类型签名,函数:FnOnce(&amp;T) -&gt; &amp;U,。它返回一个引用,而不是一个拥有的类型!

    这就是为什么我们在答案&amp;v[..] 中使用切片而不是尝试使用向量的.iter() 方法,该方法返回一个拥有的std::slice::Iter&lt;'a, T&gt;。切片是一种引用类型。

    其他想法

    好的,现在我将尝试证明为什么这个解决方案比公认的答案更好。

    首先,IntoIterator 的使用与 Rust 标准库不一致,并且可以说是 trait 的目的和意图。 trait 方法消耗 self: fn into_iter(self) -&gt; ...

    let v = vec![1,2,3,4];
    let i = v.into_iter();
    // v is no longer valid, it was moved into the iterator
    

    IntoIterator 间接用于包装器是不一致的,因为您使用的是包装器而不是集合。以我的经验,初学者将受益于遵守约定。我们应该使用普通的Iterator

    接下来,为引用 &amp;VecRefWrapper 而不是拥有的类型 VecRefWrapper 实现 IntoIterator 特征。

    假设您正在实现一个库。您的 API 的使用者将不得不使用引用运算符看似随意地装饰拥有的值,如操场上的示例所示:

    for &i in &foo.iter() {
        println!("{}", i);
    }
    

    如果您是 Rust 新手,这是一个微妙而令人困惑的区别。为什么我们必须引用该值,因为它是匿名拥有的 - 并且应该只存在于 - 循环的范围内?

    最后,上面的解决方案展示了如何通过内部可变性深入到您的数据中,并使实现mutable iterator 的路径也变得清晰。使用RefMut

    【讨论】:

    • 这是正确的方法,尤其是在深入研究结构时(例如,在编写快速文件系统时:P)
    • 公平点,但不幸的是,如果 Ref 中的结构提供了一些迭代器并且您只想转发这些迭代器,那么您的解决方案将完全没用 - 您的建议意味着您自己重新实现所有迭代器,使用唯一的变化是 Ref::map()。这也是为什么会有像 self-cell、ouroboros 和 owning-ref 这样的板条箱的原因
    【解决方案3】:

    根据我的研究,目前还没有解决此问题的方法。这里最大的问题是自引用性和 rust 不能证明你的代码是安全的事实。或者至少不是通用方式。

    如果您知道您的结构(T in Ref&lt;T&gt;)不包含任何智能指针或任何可能使您可能在“依赖”结构中获得的任何指针无效。

    请注意,self-cell 通过额外的堆分配安全地执行此操作,这在某些情况下可能没问题。

    还有 RFC for adding map_valueRef&lt;T&gt; 但正如您所看到的,通常总有一些方法可以使指针无效(这并不意味着您的具体情况是错误的,只是它可能永远不会被添加到核心库/语言,因为它不能保证任何T)

    是的,所以没有答案,抱歉。 impl IntoIterator for &amp;T 有效,但我认为它相当 hack,它迫使你写 for x in &amp;iter 而不是 for x in iter

    【讨论】:

      猜你喜欢
      • 2020-09-11
      • 2018-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-22
      • 2019-01-22
      相关资源
      最近更新 更多