[英]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.