如何在与字符串相同的结构体中存储Chars迭代器?

11
我刚开始学习Rust,但是在处理生命周期时遇到了困难。
我想要一个包含String的结构体,用于缓冲来自stdin的行。然后,我想要在该结构体上拥有一个方法,该方法返回缓冲区中的下一个字符,或者如果已经使用完该行的所有字符,则从stdin读取下一行。
文档说,Rust字符串不可通过字符索引,因为这对UTF-8效率低下。由于我按顺序访问字符,因此使用迭代器应该没问题。但是,据我所知,Rust中的迭代器与它们正在迭代的内容的生命周期相关联,我无法弄清楚如何将此迭代器存储在结构体中并与String一起使用。
以下是我想要实现的伪Rust代码。显然,它不能编译。
struct CharGetter {
    /* Buffer containing one line of input at a time */
    input_buf: String,
    /* The position within input_buf of the next character to
     * return. This needs a lifetime parameter. */
    input_pos: std::str::Chars
}

impl CharGetter {
    fn next(&mut self) -> Result<char, io::Error> {
        loop {
            match self.input_pos.next() {
                /* If there is still a character left in the input
                 * buffer then we can just return it immediately. */
                Some(n) => return Ok(n),
                /* Otherwise get the next line */
                None => {
                    io::stdin().read_line(&mut self.input_buf)?;
                    /* Reset the iterator to the beginning of the
                     * line. Obviously this doesn’t work because it’s
                     * not obeying the lifetime of input_buf */
                    self.input_pos = self.input_buf.chars();
                }
            }
        }
    }
}

我正尝试完成Synacor challenge。这涉及实现一个虚拟机,其中一个操作码从stdin读取一个字符并将其存储在寄存器中。我已经成功地完成了这部分。文档说明,每当虚拟机内的程序读取一个字符时,它将一直读取,直到读取整行。我想利用这个来添加“保存”命令到我的实现中。这意味着每当程序请求一个字符时,我将从输入中读取一行。如果该行为“save”,我将保存虚拟机的状态,然后继续获取另一行以供虚拟机使用。每次虚拟机执行输入操作码时,我需要能够从缓冲行中逐个字符地提供输入,直到缓冲区耗尽。
我的当前实现在这里。我的计划是将input_bufinput_pos添加到表示虚拟机状态的Machine结构体中。

@DK。尽管它是一个相关的重复,但它并没有涵盖这个特定情况的解决方案。 - E net4
@E_net4:这是一个重复的问题。对我来说,最让我倾向于关闭它的是“我有一个想要看到整行的理由...应该可以做到”,这听起来像任何理论解决方案都必须玩“未指定需求网球”的游戏。如果问题被修改为不是关于存储迭代器,而是关于实际问题及其要求,请联系我,我会重新打开它。 - DK.
@DK,感谢你提供的链接到另一个问题。虽然那个问题中有很多有用的信息,但我还没有能够从中推断出解决我的问题的方法。特别是,我的情况不涉及任何移动,这似乎是那个问题的主要焦点。我已经编辑了我的问题,以解释具体的问题。 - Neil Roberts
重新打开。为了上下文,此问题先前被标记为重复的:https://dev59.com/JlwY5IYBdhLWcg3ws5sF - DK.
我的情况不涉及任何移动 - 当然是的。'read_line(&mut self.input_buf)' 可能会重新分配 'String' 以增加分配,移动所有数据,使其无效化其中的任何引用(也就是 'Chars' 迭代器)。 - Shepmaster
这种情况似乎在这里有相当多的讨论:https://internals.rust-lang.org/t/self-referencing-structs/418。听起来似乎没有解决方案,除非增加一些开销。 - Neil Roberts
2个回答

9

正如在为什么我不能在同一个结构体中存储值和对该值的引用?中详细描述的那样,通常情况下你不能这样做,因为它确实是不安全的。当你移动内存时,你会使引用失效。这就是为什么很多人使用Rust - 以避免无效的引用导致程序崩溃!

让我们看一下你的代码:

io::stdin().read_line(&mut self.input_buf)?;
self.input_pos = self.input_buf.chars();

在这两行代码之间,您已经让self.input_pos处于一个糟糕的状态。如果发生恐慌,那么对象的析构函数将有机会访问无效的内存!Rust正在保护您免受大多数人从未考虑过的问题。
正如在那个答案中描述的那样:

有一种特殊情况,生命周期跟踪过于严格: 当您将某些东西放在堆上时。例如,使用 Box<T>。在这种情况下,被移动的结构体 包含指向堆的指针。所指向的值将保持不变, 但指针本身的地址将移动。实际上, 这并不重要,因为您总是遵循指针。

一些crate提供了表示此情况的方法,但它们要求 基地址永远不会移动。这排除了突变向量, 可能会导致重新分配和移动堆分配的值。

请记住,String只是添加了额外前提条件的字节向量。
而不是使用这些crate之一,我们也可以自己解决,这意味着我们(读)需要承担确保我们没有做错任何事情的所有责任。
这里的技巧是确保String内部的数据永远不会移动,也不会发生意外引用。
use std::{mem, str::Chars};

/// I believe this struct to be safe because the String is
/// heap-allocated (stable address) and will never be modified
/// (stable address). `chars` will not outlive the struct, so
/// lying about the lifetime should be fine.
///
/// TODO: What about during destruction?
///       `Chars` shouldn't have a destructor...
struct OwningChars {
    _s: String,
    chars: Chars<'static>,
}

impl OwningChars {
    fn new(s: String) -> Self {
        let chars = unsafe { mem::transmute(s.chars()) };
        OwningChars { _s: s, chars }
    }
}

impl Iterator for OwningChars {
    type Item = char;
    fn next(&mut self) -> Option<Self::Item> {
        self.chars.next()
    }
}

你甚至可以考虑把仅仅这段代码放到一个模块中,这样就不会不小心捣鼓内部了。


下面是使用ouroboros创建自引用结构体的相同代码,其中包含StringChars迭代器:

use ouroboros::self_referencing; // 0.4.1
use std::str::Chars;

#[self_referencing]
pub struct IntoChars {
    string: String,
    #[borrows(string)]
    chars: Chars<'this>,
}

// All these implementations are based on what `Chars` implements itself

impl Iterator for IntoChars {
    type Item = char;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.with_mut(|me| me.chars.next())
    }

    #[inline]
    fn count(mut self) -> usize {
        self.with_mut(|me| me.chars.count())
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.with(|me| me.chars.size_hint())
    }

    #[inline]
    fn last(mut self) -> Option<Self::Item> {
        self.with_mut(|me| me.chars.last())
    }
}

impl DoubleEndedIterator for IntoChars {
    #[inline]
    fn next_back(&mut self) -> Option<Self::Item> {
        self.with_mut(|me| me.chars.next_back())
    }
}

impl std::iter::FusedIterator for IntoChars {}

// And an extension trait for convenience

trait IntoCharsExt {
    fn into_chars(self) -> IntoChars;
}

impl IntoCharsExt for String {
    fn into_chars(self) -> IntoChars {
        IntoCharsBuilder {
            string: self,
            chars_builder: |s| s.chars(),
        }
        .build()
    }
}

使用rental crate创建一个自引用结构来包含StringChars迭代器的相同代码:

#[macro_use]
extern crate rental; // 0.5.5

rental! {
    mod into_chars {
        pub use std::str::Chars;

        #[rental]
        pub struct IntoChars {
            string: String,
            chars: Chars<'string>,
        }
    }
}

use into_chars::IntoChars;

// All these implementations are based on what `Chars` implements itself

impl Iterator for IntoChars {
    type Item = char;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.rent_mut(|chars| chars.next())
    }

    #[inline]
    fn count(mut self) -> usize {
        self.rent_mut(|chars| chars.count())
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.rent(|chars| chars.size_hint())
    }

    #[inline]
    fn last(mut self) -> Option<Self::Item> {
        self.rent_mut(|chars| chars.last())
    }
}

impl DoubleEndedIterator for IntoChars {
    #[inline]
    fn next_back(&mut self) -> Option<Self::Item> {
        self.rent_mut(|chars| chars.next_back())
    }
}

impl std::iter::FusedIterator for IntoChars {}

// And an extension trait for convenience

trait IntoCharsExt {
    fn into_chars(self) -> IntoChars;
}

impl IntoCharsExt for String {
    fn into_chars(self) -> IntoChars {
        IntoChars::new(self, |s| s.chars())
    }
}

1
这个回答没有解决将迭代器存储在与其正在迭代的对象相同的结构体中的一般问题。但是,在这种特殊情况下,我们可以通过将整数字节索引存储到字符串中来避免问题。Rust会让你使用这个字节索引创建一个字符串切片,然后我们可以从那个点开始提取下一个字符。接下来,我们只需要通过UTF-8中代码点占用的字节数更新字节索引。我们可以使用char::len_utf8()完成这个操作。
这将像以下方式工作:
struct CharGetter {
    // Buffer containing one line of input at a time
    input_buf: String,
    // The byte position within input_buf of the next character to
    // return.
    input_pos: usize,
}

impl CharGetter {
    fn next(&mut self) -> Result<char, std::io::Error> {
        loop {
            // Get an iterator over the string slice starting at the
            // next byte position in the string
            let mut input_pos = self.input_buf[self.input_pos..].chars();

            // Try to get a character from the temporary iterator
            match input_pos.next() {
                // If there is still a character left in the input
                // buffer then we can just return it immediately.
                Some(n) => {
                    // Move the position along by the number of bytes
                    // that this character occupies in UTF-8
                    self.input_pos += n.len_utf8();
                    return Ok(n);
                },
                // Otherwise get the next line
                None => {
                    self.input_buf.clear();
                    std::io::stdin().read_line(&mut self.input_buf)?;
                    // Reset the iterator to the beginning of the
                    // line.
                    self.input_pos = 0;
                }
            }
        }
    }
}

实际上,这并没有比存储迭代器更安全,因为 input_pos 变量仍然有效地执行与迭代器相同的操作,其有效性仍然取决于 input_buf 未被修改。假设在此期间有其他东西修改了缓冲区,那么当创建字符串切片时,程序可能会出现错误,因为它可能不再处于字符边界处。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接