自定义迭代器中基于可变引用的生命周期参数问题

问题描述 投票:0回答:2

我想像下面那样实现自定义迭代器,但是不能解决引用问题。

use itertools::Product;
use std::ops::Range;
struct Iter2DMut<'a, T: 'a> {
    data: &'a mut [T],
    shape: (usize, usize),
    idx_iter: Product<Range<usize>, Range<usize>>,
}

impl<'a, T: 'a> Iterator for Iter2DMut<'a, T> {
    type Item = &'a mut T;
    fn next(&mut self) -> Option<Self::Item> {
        if let Some((i, j)) = self.idx_iter.next() {
            Some(&mut self.data[i + self.shape.0 * j])
        } else {
            None
        }
    }
}

并获得以下错误消息。

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> src/main.rs:13:23
   |
13 |             Some(&mut self.data[i + self.shape.0 * j])
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
rust iterator lifetime
2个回答
2
投票

[基于作者在评论中的澄清,我假设此处的目标是迭代矩阵的矩形子矩阵。例如,给定矩阵

100  200  300  400  500  600
110  210  310  410  510  610
120  220  320  420  520  620
130  230  330  430  530  630

以行为主的切片表示

[100, 200, 300, 400, 500, 600, 110, ..., 530, 630]

我们想遍历子矩阵,例如

210  310  410  510
220  320  420  520

再次按行优先顺序排列,所以我们将得到的元素按顺序排列

210, 310, 410, 510, 220, 320, 420, 520

在这种情况下,可以使用安全Rust相对有效地解决此问题。技巧是在split_at_mutdata字段中使用切片的Iter2DMut方法,以便根据需要一次剥离一个可变参考。随着迭代的进行,data字段将更新为越来越小的片,以使其不再包含已被迭代的元素;这是必要的,因为在任何给定的迭代中,Rust都不允许我们生成对元素的可变引用,同时还保留包含该元素的可变切片。通过更新切片,我们可以确保它始终与先前所有对next()的调用所生成的可变引用不相交,从而满足Rust借阅检查器的要求。这是可以做到的:

use itertools::{Itertools, Product};
use std::ops::Range;
use std::mem;

struct Iter2DMut<'a, T: 'a> {
    data: &'a mut [T],
    full_shape: (usize, usize),
    sub_shape: (usize, usize),
    idx_iter: Product<Range<usize>, Range<usize>>,
}

impl<'a, T> Iter2DMut<'a, T> {
    fn new(
        data: &'a mut [T],
        full_shape: (usize, usize),
        sub_shape: (usize, usize),
        offset: (usize, usize),
    ) -> Self {
        assert!(full_shape.0 * full_shape.1 == data.len());
        assert!(offset.0 + sub_shape.0 <= full_shape.0);
        assert!(offset.1 + sub_shape.1 <= full_shape.1);
        Iter2DMut {
            data: &mut data[offset.0 * full_shape.1 + offset.1 ..],
            full_shape,
            sub_shape,
            idx_iter: (0..sub_shape.0).cartesian_product(0..sub_shape.1)
        }
    }
}

impl<'a, T: 'a> Iterator for Iter2DMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some((_, j)) = self.idx_iter.next() {
            let mut data: &'a mut [T] = &mut [];
            mem::swap(&mut self.data, &mut data);
            let (first, rest) = data.split_at_mut(1);
            data = rest;
            if j == self.sub_shape.1 - 1 {
                let n_skip = self.full_shape.1 - self.sub_shape.1;
                let (_, rest) = data.split_at_mut(n_skip);
                data = rest;
            }
            self.data = data;
            Some(&mut first[0])
        } else {
            None
        }
    }
}

fn main() {
    let mut v: Vec<usize> = vec![
        100, 200, 300, 400, 500, 600,
        110, 210, 310, 410, 510, 610,
        120, 220, 320, 420, 520, 620,
        130, 230, 330, 430, 530, 630,
    ];
    for x in Iter2DMut::new(&mut v, (4, 6), (2, 4), (1, 1)) {
        println!("{}", x);
    }
}

这里还有另外一个技巧:我们使用mem::swapdata中移出Iter2DMut字段,以便在其上调用split_at_mut。我们暂时交换一个虚拟值&mut [];这是必要的,因为Rust不允许我们(随机地)从(可变地)借来的结构中移出一个值,而不能同时放回一些东西。另一方面,如果我们没有尝试移出data,而是像split_at_mut那样直接调用了self.data.split_at_mut(1),它将使借阅检查程序失败,因为那样的话,我们就应该借入self.data它的生存时间与&mut self方法中的next参考输入一样长,而不一定与我们需要的'a生存期一样长。


0
投票

Edit:这是对在可变引用上创建迭代器的问题的更一般的解释。 Brent's answer显示了如何使用std中的函数来为您解决不安全的指针操作,以解决此特定问题。


迭代可变引用需要不安全的代码somewhere。要了解原因,请考虑一个更简单的示例:

struct MyIterMut<'a, T: 'a> {
    data: &'a mut [T],
    index: usize,
}

impl<'a, T: 'a> Iterator for MyIterMut<'a, T> {
    type Item = &'a mut T;
    fn next(&mut self) -> Option<Self::Item> {
        unimplemented!()
    }
}

fn main() {
    let mut data = vec![1, 2, 3, 4];

    let a;
    let b;
    {
        let mut iter = MyIterMut { data: &mut data, index: 0 };
        a = iter.next();
        b = iter.next();
    }

    // a and b  are usable after the iterator is dropped, as long as data is still around
    println!("{:?}, {:?}", a, b);
}

该迭代器的用户被允许在删除迭代器后使用其值,只要原始数据仍然有效。这用next的类型表示,加上明确的生命周期,为:

fn next<'n>(&'n mut self) -> Option<&'a mut T>

'n'a之间没有关系,因此使用迭代器的代码可以不受限制地自由使用它们。这就是您想要的。

假设我们这样实现next()

fn next(&mut self) -> Option<&'a mut T> {
    Some(&mut self.data[0])
}

此实现是错误的,并且会导致您在代码中看到的相同错误。如果编译器允许,则上面的main函数将具有两个变量ab,它们都包含对同一数据的可变引用。这是未定义的行为,借用检查器可以防止它发生。

防止这种情况的方法是,注意到您是从self借用的,它的生存期与数据的生存期无关。编译器无法知道是否将多次调用next或调用者将如何处理数据。它只知道没有足够的信息来确定它是否安全。

但是,您可能会争辩,您不需要[[需要借用整个self;您只需要从切片中借用该单个项目。不幸的是,当您借用一个结构片段时,您将借用整个结构。无法表示这种调用next()的类型将借用索引0,而下一个将借用索引1等。

鉴于您知道您的实现只需要借用一次每个索引,您可以使用原始指针,而只是告诉借用检查器您知道自己在做什么:

impl<'a, T: 'a> Iterator for MyIterMut<'a, T> { type Item = &'a mut T; fn next(&mut self) -> Option<Self::Item> { if self.index < self.data.len() { let index = self.index; self.index += 1; let ptr = self.data.as_mut_ptr(); Some(unsafe { &mut *ptr.add(index) }) } else { None } } }

由于迭代器采用对&mutdata引用,因此无法构造它的多个实例。如果可能的话,仍然会有不确定行为的可能性,但是Rust借用检查器会为我们解决这个问题。


[每当使用不安全的代码时,您都必须非常小心如何实施假定的不变式。

根据您的情况,您还需要确保由于shape与数据大小不匹配而无法创建无效的指针。如果发生这种情况,您可能应该panic!,它总是比未定义的行为更可取。

我希望这个答案的长度能传达出您不应轻描淡写。

© www.soinside.com 2019 - 2024. All rights reserved.