如何将Chars迭代器存储在与其迭代的String相同的结构中?

时间:2017-05-13 10:37:18

标签: rust

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

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

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

2 个答案:

答案 0 :(得分:3)

正如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<T>。在这种情况下,移动的结构   包含指向堆的指针。指向的值将保持不变   稳定,但指针本身的地址会移动。在实践中,   这没关系,因为你总是按照指针。

     

rental crateowning_ref crate是表示此问题的方法   case,但它们要求基地址永远不会移动。这个规则   变异向量,这可能导致重新分配和移动   堆分配值。

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

我们也可以推出自己的解决方案,而不是使用其中一个包,这意味着我们(阅读)接受所有责任,以确保我们没有做错任何事情。

这里的技巧是确保String内的数据永远不会移动,不会引起意外引用。

use std::str::Chars;
use std::mem;

/// 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()
    }
}

你甚至可以考虑将只是这个代码放入一个模块中,这样你就不会意外地弄乱内部。

以下是使用rental crate创建包含StringChars迭代器的自引用结构的相同代码:

#[macro_use]
extern crate rental;

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 :(得分:0)

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