【问题标题】:How can I store a Chars iterator in the same struct as the String it is iterating on?如何将 Chars 迭代器存储在与其迭代的 String 相同的结构中?
【发布时间】:2017-05-13 10:37:18
【问题描述】:

我刚刚开始学习 Rust,我正在努力处理生命周期。

我想要一个带有String 的结构,用于缓冲来自标准输入的行。然后我想在结构上有一个方法,它从缓冲区返回下一个字符,或者如果该行中的所有字符都已被消耗,它将从 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。这涉及实现一个虚拟机,其中一个操作码从标准输入读取一个字符并将其存储在寄存器中。我的这部分工作正常。该文档指出,每当 VM 内的程序读取一个字符时,它都会继续读取,直到它读取整行。我想利用这一点在我的实现中添加一个“保存”命令。这意味着每当程序要求输入字符时,我都会从输入中读取一行。如果该行是“保存”,我将保存 VM 的状态,然后继续获取另一行以馈送到 VM。每次 VM 执行输入操作码时,我需要能够从缓冲的行一次给它一个字符,直到缓冲区耗尽。

我当前的实现是here。我的计划是将input_bufinput_pos 添加到代表VM 状态的Machine 结构中。

【问题讨论】:

  • @DK。虽然它是一个相关的副本,但它并没有涵盖这种特殊情况的解决方案。
  • @E_net4:这是一个重复的问题。对我来说,最重要的是“我有理由想看到整条线......这应该是可行的”,这听起来像任何理论解决方案都必须打未指定要求的网球。如果问题被修改为不是关于存储迭代器,而是关于 实际 问题及其要求,请联系我,我会重新打开。
  • @DK。感谢您提供另一个问题的链接。尽管它有很多有用的信息,但我无法从中推断出我的问题的解决方案。特别是我的案例不涉及任何移动,这似乎是该问题的重点。我已经编辑了我的问题以解释确切的问题。
  • 重新打开。就上下文而言,这个问题之前被标记为重复:stackoverflow.com/questions/32300132/…
  • 我的案子不涉及任何搬家——当然可以。 read_line(&amp;mut self.input_buf) 可以重新分配String 以增加分配,移动所有数据,使对其的任何引用无效(又名Chars 迭代器)。

标签: rust


【解决方案1】:

正如Why can't I store a value and a reference to that value in the same struct? 中详细描述的那样,通常您不能这样做,因为它确实不安全。移动内存时,引用无效。这就是很多人使用 Rust 的原因 - 没有导致程序崩溃的无效引用!

让我们看看你的代码:

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

在这两行之间,您使self.input_pos 处于错误状态。如果发生恐慌,那么对象的析构函数就有机会访问无效内存! Rust 正在保护您免受大多数人从未考虑过的问题的影响。


正如该答案中描述的

有一种特殊情况是生命周期跟踪过于热心: 当你有东西放在堆上时。当您使用 Box&lt;T&gt;,例如。在这种情况下,被移动的结构 包含指向堆的指针。指向的值将保持不变 稳定,但指针本身的地址会移动。在实践中, 这没关系,因为你总是跟着指针走。

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

请记住,String 只是一个添加了额外前提条件的字节向量。

除了使用其中一个板条箱外,我们还可以推出自己的解决方案,这意味着我们(请阅读)承担确保我们没有做错任何事情的所有责任。

这里的诀窍是确保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 crate 创建包含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())
    }
}

【讨论】:

    【解决方案2】:

    这个答案没有解决试图将迭代器存储在与它迭代的对象相同的结构中的一般问题。但是,在这种特殊情况下,我们可以通过将整数字节索引存储到字符串而不是迭代器来解决这个问题。 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 未被修改。据推测,如果在此期间有其他东西修改了缓冲区,那么程序可能会在创建字符串切片时出现恐慌,因为它可能不再位于字符边界处。

    【讨论】:

      猜你喜欢
      • 2022-06-30
      • 1970-01-01
      • 2015-01-25
      • 1970-01-01
      • 1970-01-01
      • 2015-07-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-22
      相关资源
      最近更新 更多