rust

rust - ¿Cómo puedo almacenar un iterador Chars en la misma estructura que la cadena en la que está iterando?



(1)

Estoy empezando a aprender Rust y estoy luchando por manejar las vidas.

Me gustaría tener una estructura con una String que se utilizará para amortiguar las líneas de stdin. Entonces me gustaría tener un método en la estructura que devuelva el siguiente carácter del búfer, o si se han consumido todos los caracteres de la línea, leerá la siguiente línea de stdin.

La documentación dice que las cadenas de óxido no son indexables por caracteres porque eso es ineficiente con UTF-8. Como accedo a los caracteres secuencialmente, debería estar bien usar un iterador. Sin embargo, por lo que entiendo, los iteradores en Rust están vinculados a la vida útil de la cosa que están iterando y no puedo entender cómo podría almacenar este iterador en la estructura junto a la String .

Aquí está el pseudo-Rust que me gustaría lograr. Obviamente no se compila.

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

Estoy tratando de hacer el desafío de Synacor . Esto implica implementar una máquina virtual donde uno de los códigos de operación lee un carácter de stdin y lo almacena en un registro. Tengo esta parte funcionando bien. La documentación establece que siempre que el programa dentro de la VM lea un carácter, seguirá leyendo hasta que lea una línea completa. Quería aprovechar esto para agregar un comando "guardar" a mi implementación. Eso significa que siempre que el programa solicite un carácter, leeré una línea de la entrada. Si la línea es "guardar", guardaré el estado de la VM y luego continuaré obteniendo otra línea para alimentarla. Cada vez que la máquina virtual ejecuta el código de operación de entrada, necesito poder darle un carácter a la vez desde la línea almacenada hasta que se agote el almacenamiento intermedio.

Mi implementación actual está here . Mi plan era agregar input_buf y input_pos a la estructura de la Machine que representa el estado de la máquina virtual.


Como se describe a fondo en ¿Por qué no puedo almacenar un valor y una referencia a ese valor en la misma estructura? , en general, no puede hacer esto porque realmente no es seguro . Cuando mueve la memoria, invalida las referencias. Esta es la razón por la cual mucha gente usa Rust, ¡para no tener referencias no válidas que provoquen bloqueos del programa!

Veamos tu código:

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

Entre estas dos líneas, has dejado self.input_pos en mal estado. Si se produce un pánico, ¡el destructor del objeto tiene la oportunidad de acceder a memoria no válida! Rust te protege de un problema en el que la mayoría de la gente nunca piensa.

Como también se describe en esa respuesta:

Hay un caso especial en el que el seguimiento de por vida es demasiado celoso: cuando tienes algo colocado en el montón. Esto ocurre cuando utiliza un Box<T> , por ejemplo. En este caso, la estructura que se mueve contiene un puntero en el montón. El valor señalado permanecerá estable, pero la dirección del puntero se moverá. En la práctica, esto no importa, ya que siempre sigues el puntero.

La caja de alquiler o la caja owning_ref son formas de representar este caso, pero requieren que la dirección base nunca se mueva . Esto descarta los vectores mutantes, que pueden causar una reasignación y un movimiento de los valores asignados en el montón.

Recuerde que una String es solo un vector de bytes con precondiciones adicionales agregadas.

En lugar de usar una de esas cajas, también podemos lanzar nuestra propia solución, lo que significa que (leímos) podemos aceptar toda la responsabilidad de garantizar que no estamos haciendo nada malo.

El truco aquí es asegurarse de que los datos dentro de la String nunca se muevan y no se tomen referencias accidentales.

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

Incluso podría pensar en poner solo este código en un módulo para que no pueda perder el tiempo accidentalmente con las entrañas.

Aquí está el mismo código que usa la caja de alquiler para crear una estructura autorreferencial que contiene el iterador String y Chars :

#[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()) } }