如何在结构字段上创建可变迭代器

How do I create mutable iterator over struct fields

提问人:Vinnie 提问时间:5/24/2020 最后编辑:StargateurVinnie 更新时间:5/24/2020 访问量:2350

问:

所以我正在使用 Rust 开发一个小型 NES 模拟器,并且我正在尝试使用我的状态寄存器。寄存器是一个结构,它包含一些包含布尔值的字段(标志),寄存器本身是 CPU 结构的一部分。现在,我想遍历这些字段并根据我执行的一些指令设置布尔值。但是,我无法实现可变迭代器,我已经实现了一个 into_iter() 函数并且能够遍历字段以获取/打印布尔值,但是如何在结构体本身中更改这些值?这甚至可能吗?

pub struct StatusRegister {
    CarryFlag: bool,
    ZeroFlag: bool,
    OverflowFlag: bool,
}

impl StatusRegister {
    fn new() -> Self {
        StatusRegister {
            CarryFlag: true,
            ZeroFlag: false,
            OverflowFlag: true,
        }
    }
}

impl<'a> IntoIterator for &'a StatusRegister {
    type Item = bool;
    type IntoIter = StatusRegisterIterator<'a>;

    fn into_iter(self) -> Self::IntoIter {
        StatusRegisterIterator {
            status: self,
            index: 0,
        }
    }
}

pub struct StatusRegisterIterator<'a> {
    status: &'a StatusRegister,
    index: usize,
}

impl<'a> Iterator for StatusRegisterIterator<'a> {
    type Item = bool;

    fn next(&mut self) -> Option<bool> {
        let result = match self.index {
            0 => self.status.CarryFlag,
            1 => self.status.ZeroFlag,
            2 => self.status.OverflowFlag,
            _ => return None,
        };
        self.index += 1;
        Some(result)
    }
}

pub struct CPU {
    pub memory: [u8; 0xffff],
    pub status: StatusRegister,
}

impl CPU {
    pub fn new() -> CPU {
        let memory = [0; 0xFFFF];
        CPU {
            memory,
            status: StatusRegister::new(),
        }
    }

    fn execute(&mut self) {
        let mut shifter = 0b1000_0000;
        for status in self.status.into_iter() {
            //mute status here!
            println!("{}", status);
            shifter <<= 1;
        }
    }
}

fn main() {
    let mut cpu = CPU::new();
    cpu.execute();
}
结构 参考 迭 代 可变

评论

1赞 Stargateur 5/24/2020
这回答了你的问题吗?如何编写返回对自身引用的迭代器?
0赞 Stargateur 5/24/2020
重复项没有说的是,在你的情况下,你希望有很大的不同,因为引用实现复制,但可变引用没有。因此,编译器需要借用迭代器相同的生命周期,以确保可变引用只借用一次。但你不能这样做,因为 Rust 没有 GAT。我认为 std 使用他们的可变迭代器实现是不安全的。&mut&'a
1赞 trent 5/24/2020
我不认为重复是合适的。预期的行为不是实现流式迭代器,而只是实现返回对原始数据结构的引用的迭代器。《服务贸易总协定》无关紧要。这个问题本质上与(除了可以在安全代码中解决它,如答案以两种不同的方式显示的那样)没有什么不同。slice::IterMut
0赞 Stargateur 5/24/2020
@trentcl但是切片 IterMut 的问题是 GAT 没有吗?
0赞 trent 5/24/2020
@Stargateur 不,只是很难在安全代码中实现,但在类型系统中很容易表达。IterMut

答:

9赞 SCappella 5/24/2020 #1

一般来说,在可变引用上实现迭代器是很困难的。如果迭代器两次返回对同一元素的引用,则会变得不合理。这意味着,如果你想用纯安全的代码编写一个,你必须以某种方式说服编译器,每个元素只被访问一次。这排除了简单地使用索引:您可能总是忘记递增索引或将其设置在某个地方,编译器将无法对其进行推理。


一种可能的解决方法是将几个 s 链接在一起(每个引用对应一个要迭代的引用)。std::iter::once

例如

impl StatusRegister {
    fn iter_mut(&mut self) -> impl Iterator<Item = &mut bool> {
        use std::iter::once;
        once(&mut self.CarryFlag)
            .chain(once(&mut self.ZeroFlag))
            .chain(once(&mut self.OverflowFlag))
    }
}

(游乐场)

优点:

  • 实现起来相当简单。
  • 没有分配。
  • 没有外部依赖关系。

缺点:

  • 迭代器有一个非常复杂的类型:。std::iter::Chain<std::iter::Chain<std::iter::Once<&mut bool>, std::iter::Once<&mut bool>>, std::iter::Once<&mut bool>>

因此,如果您不想使用 ,则必须在代码中包含它。这包括实现 for ,因为您必须明确指示类型是什么。impl Iterator<Item = &mut bool>IntoIterator&mut StatusRegisterIntoIter


另一种方法是使用数组或保存所有可变引用(具有正确的生存期),然后委托给其迭代器实现以获取值。例如Vec

impl StatusRegister {
    fn iter_mut(&mut self) -> std::vec::IntoIter<&mut bool> {
        vec![
            &mut self.CarryFlag,
            &mut self.ZeroFlag,
            &mut self.OverflowFlag,
        ]
        .into_iter()
    }
}

(游乐场)

优点:

  • 该类型更易于管理。std::vec::IntoIter<&mut bool>
  • 实现起来仍然相当简单。
  • 没有外部依赖关系。

缺点:

  • 每次调用都需要分配。iter_mut

我还提到使用数组。这将避免分配,但事实证明,数组尚未在其值上实现迭代器,因此上面使用 a 而不是 a 的代码将不起作用。但是,存在为大小有限的固定长度数组实现此功能的板条箱,例如 arrayvec(或 array_vec)。[&mut bool; 3]Vec<&mut bool>

优点:

  • 无分配。
  • 简单的迭代器类型。
  • 易于实现。

缺点:

  • 外部依赖。

我要讨论的最后一种方法是使用 .由于与其他方法相比,这没有太多好处,因此我一般不推荐它。这主要是为了向您展示如何实现这一点unsafe

与原始代码一样,我们将在自己的结构上实现。Iterator

impl<'a> IntoIterator for &'a mut StatusRegister {
    type IntoIter = StatusRegisterIterMut<'a>;
    type Item = &'a mut bool;

    fn into_iter(self) -> Self::IntoIter {
        StatusRegisterIterMut {
            status: self,
            index: 0,
        }
    }
}

pub struct StatusRegisterIterMut<'a> {
    status: &'a mut StatusRegister,
    index: usize,
}

不安全来自方法,我们必须(基本上)将类型转换为 ,这通常是不安全的。但是,只要我们确保不允许这些可变引用别名,我们应该没问题。可能还有其他一些微妙的问题,所以我不保证这是合理的。值得一提的是,MIRI没有发现任何问题。next&mut &mut T&mut Tnext

impl<'a> Iterator for StatusRegisterIterMut<'a> {
    type Item = &'a mut bool;

    // Invariant to keep: index is 0, 1, 2 or 3
    // Every call, this increments by one, capped at 3
    // index should never be 0 on two different calls
    // and similarly for 1 and 2.
    fn next(&mut self) -> Option<Self::Item> {
        let result = unsafe {
            match self.index {
                // Safety: Since each of these three branches are
                // executed exactly once, we hand out no more than one mutable reference
                // to each part of self.status
                // Since self.status is valid for 'a
                // Each partial borrow is also valid for 'a
                0 => &mut *(&mut self.status.CarryFlag as *mut _),
                1 => &mut *(&mut self.status.ZeroFlag as *mut _),
                2 => &mut *(&mut self.status.OverflowFlag as *mut _),
                _ => return None
            }
        };
        // If self.index isn't 0, 1 or 2, we'll have already returned
        // So this bumps us up to 1, 2 or 3.
        self.index += 1;
        Some(result)
    }
}

(游乐场)

优点:

  • 没有分配。
  • 简单的迭代器类型名称。
  • 没有外部依赖关系。

缺点:

  • 实施起来很复杂。要成功使用 ,您需要非常熟悉什么是允许的,什么是不允许的。到目前为止,这部分答案花了我最长的时间来确保我没有做错什么。unsafe
  • 不安全因素会感染模块。在定义此迭代器的模块中,我可以通过弄乱 的 or 字段来“安全地”导致不健全。唯一允许封装的是,在此模块之外,这些字段是不可见的。statusindexStatusRegisterIterMut

评论

0赞 Stargateur 5/24/2020
我仍然认为 GAT 会更好地解决这个问题,但总结了所有当前技巧的好答案。我认为向量或数组 1 在简单性和效率方面可能是最好的,因为链式 1 可能有点昂贵,因为它是 O(n),而切片 len 是 O(1)。请注意,不安全的版本也是 O(n),但编译器可以很容易地优化它。
0赞 trent 5/24/2020
@Stargateur 在这种情况式迭代器版本严格来说比普通迭代器差,因为它仍然可以可变地借用原始结构,但不能被编辑。 GAT 解决了类型系统当前表达能力不足的问题,但在这种情况下,类型系统已经有足够的表现力,困难在于实现。collect
0赞 Stargateur 5/24/2020
@trentcl 我不同意你对这个问题的理解。GAT 将允许收集迭代器,我不明白为什么它实际上不应该。GAT 将允许借用迭代器的可能性,从而确保可变借用只存在一次。所以“在这种情况下,类型系统已经足够富有表现力了”我不同意,或者我错过了一些东西。
0赞 trent 5/24/2020
@Stargateur 无法收集任何流式迭代器。API 不允许它(或其他常见用途,如 )。流式迭代器是非流式迭代器的泛化(因为它们可以写入非流式迭代器不能写入的地方),但它们的能力较差(因为它们不能做非流式迭代器可以做的所有事情)。let first = iter.next(); let second = iter.next(); foo(first, second);