【问题标题】:Borrow checker issue in `zip`-like function with a callback带有回调的类似“zip”的函数中的借用检查器问题
【发布时间】:2017-07-25 16:31:50
【问题描述】:

我正在尝试实现一个函数,该函数同时通过两个迭代器,为每一对调用一个函数。这个回调可以通过返回一个(bool, bool) 元组来控制在每一步中哪个迭代器被推进。由于迭代器在我的用例中引用了缓冲区,因此它们无法实现 stdlib 中的 Iterator 特征,而是通过 next_ref 函数使用,该函数与 Iterator::next 相同,但需要一个额外的生命周期参数。

// An iterator-like type, that returns references to itself
// in next_ref
struct RefIter {
    value: u64
}

impl RefIter {
    fn next_ref<'a>(&'a mut self) -> Option<&'a u64> {
        self.value += 1;
        Some(&self.value)
    }
}

// Iterate over two RefIter simultaneously and call a callback
// for each pair. The callback returns a tuple of bools
// that indicate which iterators should be advanced.
fn each_zipped<F>(mut iter1: RefIter, mut iter2: RefIter, callback: F)
    where F: Fn(&Option<&u64>, &Option<&u64>) -> (bool, bool)
{
    let mut current1 = iter1.next_ref();
    let mut current2 = iter2.next_ref();
    loop {
        let advance_flags = callback(&current1, &current2);
        match advance_flags {
            (true, true) => {
                current1 = iter1.next_ref();
                current2 = iter2.next_ref();
            },
            (true, false) => {
                current1 = iter1.next_ref();
            },
            (false, true) => {
                current2 = iter1.next_ref();
            },
            (false, false) => {
                return
            }
        }
    }
}

fn main() {
    let mut iter1 = RefIter { value: 3 };
    let mut iter2 = RefIter { value: 4 };
    each_zipped(iter1, iter2, |val1, val2| {
        let val1 = *val1.unwrap();
        let val2 = *val2.unwrap();
        println!("{}, {}", val1, val2);
        (val1 < 10, val2 < 10)
    });
}
error[E0499]: cannot borrow `iter1` as mutable more than once at a time
  --> src/main.rs:28:28
   |
22 |     let mut current1 = iter1.next_ref();
   |                        ----- first mutable borrow occurs here
...
28 |                 current1 = iter1.next_ref();
   |                            ^^^^^ second mutable borrow occurs here
...
42 | }
   | - first borrow ends here

error[E0499]: cannot borrow `iter2` as mutable more than once at a time
  --> src/main.rs:29:28
   |
23 |     let mut current2 = iter2.next_ref();
   |                        ----- first mutable borrow occurs here
...
29 |                 current2 = iter2.next_ref();
   |                            ^^^^^ second mutable borrow occurs here
...
42 | }
   | - first borrow ends here

error[E0499]: cannot borrow `iter1` as mutable more than once at a time
  --> src/main.rs:32:28
   |
22 |     let mut current1 = iter1.next_ref();
   |                        ----- first mutable borrow occurs here
...
32 |                 current1 = iter1.next_ref();
   |                            ^^^^^ second mutable borrow occurs here
...
42 | }
   | - first borrow ends here

error[E0499]: cannot borrow `iter1` as mutable more than once at a time
  --> src/main.rs:35:28
   |
22 |     let mut current1 = iter1.next_ref();
   |                        ----- first mutable borrow occurs here
...
35 |                 current2 = iter1.next_ref();
   |                            ^^^^^ second mutable borrow occurs here
...
42 | }
   | - first borrow ends here

我明白它为什么会抱怨,但找不到解决方法。如有任何关于此主题的帮助,我将不胜感激。

playground 中指向此 sn-p 的链接。

【问题讨论】:

    标签: rust borrow-checker


    【解决方案1】:

    由于迭代器在我的用例中引用了缓冲区,因此它们无法实现 stdlib 中的 Iterator 特征,而是通过 next_ref 函数使用,该函数与 Iterator::next 相同,但需要一个额外的生命周期参数。

    您正在描述一个流式迭代器。为此有一个板条箱,恰当地称为streaming_iterator。该文档描述了您的问题(强调我的问题):

    虽然标准 Iterator trait 的功能基于 next 方法,StreamingIterator 的功能基于 一对方法:advanceget。这基本上分裂了 next的逻辑一半(其实就是StreamingIteratornext方法 除了调用advance 后跟get 之外什么都不做。

    这是必需的,因为 Rust 对借用的词法处理(更多 特别是缺乏单次进入,多次退出借用)。 如果 StreamingIterator 被定义为 Iterator 只需要一个 next 方法,像filter 这样的操作将无法定义。

    箱子目前没有zip 函数,当然也不是你描述的变体。但是,它很容易实现:

    extern crate streaming_iterator;
    
    use streaming_iterator::StreamingIterator;
    
    fn each_zipped<A, B, F>(mut iter1: A, mut iter2: B, callback: F)
    where
        A: StreamingIterator,
        B: StreamingIterator,
        F: for<'a> Fn(Option<&'a A::Item>, Option<&'a B::Item>) -> (bool, bool),
    {
        iter1.advance();
        iter2.advance();
    
        loop {
            let advance_flags = callback(iter1.get(), iter2.get());
            match advance_flags {
                (true, true) => {
                    iter1.advance();
                    iter2.advance();
                }
                (true, false) => {
                    iter1.advance();
                }
                (false, true) => {
                    iter1.advance();
                }
                (false, false) => return,
            }
        }
    }
    
    struct RefIter {
        value: u64
    }
    
    impl StreamingIterator for RefIter {
        type Item = u64;
    
        fn advance(&mut self) {
            self.value += 1;
        }
    
        fn get(&self) -> Option<&Self::Item> {
            Some(&self.value)
        }
    }
    
    fn main() {
        let iter1 = RefIter { value: 3 };
        let iter2 = RefIter { value: 4 };
        each_zipped(iter1, iter2, |val1, val2| {
            let val1 = *val1.unwrap();
            let val2 = *val2.unwrap();
            println!("{}, {}", val1, val2);
            (val1 < 10, val2 < 10)
        });
    }
    

    【讨论】:

    • 最后一个iter1.advance();应该是iter2.advance();吗?
    • @Douglas 看起来确实像,但我只是遵循 OP 的逻辑。想必他们有办法解决他们的疯狂。
    • 没有办法解决这种特别的疯狂,这是我从实际代码中删除内容时犯的复制和粘贴错误。 zip 的特定版本的原因是 fastq 记录(有关 DNA 的 sn-p 的信息)通常成对出现,被分成两个不同的文件。但有时这些文件包含几个未配对的记录,这会导致普通 zip 不同步。顺便说一下,拆分迭代器是个好主意。我想我错过了关键字streaming iterator,这使得谷歌搜索变得非常困难。谢谢。
    【解决方案2】:

    这段代码的问题在于RefIter 被以两种方式使用,它们从根本上是相互矛盾的:

    • next_ref 的调用者收到对存储值的引用,该引用与RefIter 的生命周期相关联
    • RefIter 的值需要是可变的,以便在每次调用时递增

    这完美地描述了可变别名(您试图在持有对它的引用时修改“值”)——这是 Rust 明确设计要防止的。

    为了使each_zipped 工作,您需要修改RefIter 以避免分发对您希望变异的数据的引用。 我使用RefCellRc 的组合实现了以下一种可能性:

    use std::cell::RefCell;
    use std::rc::Rc;
    
    // An iterator-like type, that returns references to itself
    // in next_ref
    struct RefIter {
        value: RefCell<Rc<u64>>
    }
    
    
    impl RefIter {
        fn next_ref(&self) -> Option<Rc<u64>> {
            let new_val = Rc::new(**self.value.borrow() + 1);
            *self.value.borrow_mut() = new_val;
            Some(Rc::clone(&*self.value.borrow()))
        }
    }
    
    
    // Iterate over two RefIter simultaniously and call a callback
    // for each pair. The callback returns a tuple of bools
    // that indicate which iterators should be advanced.
    fn each_zipped<F>(iter1: RefIter, iter2: RefIter, callback: F)
        where F: Fn(&Option<Rc<u64>>, &Option<Rc<u64>>) -> (bool, bool)
    {
        let mut current1 = iter1.next_ref();
        let mut current2 = iter2.next_ref();
        loop {
            let advance_flags = callback(&current1, &current2);
            match advance_flags {
                (true, true) => {
                    current1 = iter1.next_ref();
                    current2 = iter2.next_ref();
                },
                (true, false) => {
                    current1 = iter1.next_ref();
                },
                (false, true) => {
                    current2 = iter1.next_ref();
                },
                (false, false) => {
                    return
                }
            }
        }
    }
    
    fn main() {
        let iter1 = RefIter { value: RefCell::new(Rc::new(3)) };
        let iter2 = RefIter { value: RefCell::new(Rc::new(4)) };
        each_zipped(iter1, iter2, |val1, val2| {
            // We can't use unwrap() directly, since we're only passed a reference to an Option
            let val1 = **val1.iter().next().unwrap();
            let val2 = **val2.iter().next().unwrap();
            println!("{}, {}", val1, val2);
            (val1 < 10, val2 < 10)
        });
    }
    

    此版本的RefIterRcs 分发给消费者,而不是参考。这避免了可变别名的问题 - 更新 value 是通过放置 一个新的Rc 到外部RefCell。这样做的一个副作用是,即使在RefIter 已经前进之后,消费者也能够保持对缓冲区的“旧”引用(通过返回的Rc)。

    【讨论】:

    • 我认为您认为RefIter 本身的设计存在缺陷是不正确的。我们可以轻松地一次遍历一个迭代器:gist.github.com/anonymous/b43d24bbd5649b62d0b9612cfa9bc01a。当使用 Rc 时,我也可以使用普通的迭代器,但在我的用例中,我想返回对同一缓冲区的不同切片的引用(它也会随着新数据的读取而改变)。
    • 这个问题似乎与非词汇生命周期有关。我们在each_zipped 的开头采用了对current1 的可变引用。然后我们使用该值来调用回调。在这一点上,我们不再需要参考。我们应该可以再次调用next_ref,但编译器似乎没有注意到这一点。
    • 我认为你对非词法生命周期的看法是正确的——current1current2 的生命周期延伸到函数的末尾,即使它们在 callback 之后没有使用。
    猜你喜欢
    • 1970-01-01
    • 2023-01-25
    • 1970-01-01
    • 2011-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-05
    • 2019-12-03
    相关资源
    最近更新 更多