[英]How can I store a Chars iterator in the same struct as the String it is iterating on?
我刚刚开始学习 Rust,我正在努力处理生命周期。
我想要一个带有String
的结构,它将用于缓冲来自标准输入的行。 然后我想在结构上有一个方法,它从缓冲区返回下一个字符,或者如果该行中的所有字符都已被消耗,它将从标准输入读取下一行。
文档说 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 挑战。 这涉及实现一个虚拟机,其中一个操作码从 stdin 读取字符并将其存储在寄存器中。 我有这部分工作正常。 文档指出,无论何时 VM 内的程序读取一个字符,它都会一直读取直到读取整行。 我想利用这一点在我的实现中添加一个“保存”命令。 这意味着每当程序要求输入一个字符时,我都会从输入中读取一行。 如果该行是“save”,我将保存 VM 的状态,然后继续获取另一行以提供给 VM。 每次 VM 执行输入操作码时,我需要能够从缓冲行开始一次给它一个字符,直到缓冲区耗尽。
我当前的实现是here 。 我的计划是将input_buf
和input_pos
添加到表示 VM 状态的Machine
结构中。
正如为什么我不能在同一个结构中存储值和对该值的引用中的详细描述? ,通常你不能这样做,因为它确实是不安全的。 移动内存时,会使引用无效。 这就是为什么很多人使用 Rust - 没有导致程序崩溃的无效引用!
让我们看看你的代码:
io::stdin().read_line(&mut self.input_buf)?;
self.input_pos = self.input_buf.chars();
在这两行之间,您让self.input_pos
处于糟糕的状态。 如果发生panic,那么对象的析构函数就有机会访问无效内存! Rust 正在保护您免受大多数人从未考虑过的问题。
作为这个问题的答案也描述:
有一种特殊情况,生命周期跟踪过于热情:当你在堆上放置了一些东西时。 例如,当您使用
Box<T>
时会发生这种情况。 在这种情况下,被移动的结构包含一个指向堆的指针。 指向的值将保持稳定,但指针本身的地址将移动。 在实践中,这无关紧要,因为您始终遵循指针。一些 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 创建包含String
和Chars
迭代器的自引用结构的相同代码:
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()
}
}
这是使用租用箱创建包含String
和Chars
迭代器的自引用结构的相同代码:
#[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())
}
}
这个答案没有解决尝试将迭代器存储在与它正在迭代的对象相同的结构中的一般问题。 但是,在这种特殊情况下,我们可以通过将整数字节索引而不是迭代器存储到字符串中来解决这个问题。 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
未被修改。 据推测,如果在此期间有其他东西修改了缓冲区,那么在创建字符串切片时程序可能会发生混乱,因为它可能不再位于字符边界处。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.