Iterar condicionalmente sobre uno de varios iteradores posibles.

Resuelto Linear asked hace 9 años • 4 respuestas

Estoy intentando cambiar el comportamiento en función de una Optionentrada a una función. La idea es iterar en función de si un determinado Optionestá presente o no. Aquí hay un ejemplo mínimo, aunque tonto:

use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    for i in match x {
        None => 1..5,
        Some(x) => iter::repeat(x).take(5),
    } {
        println!("{}", i);
    }
}

Recibo un error:

error[E0308]: match arms have incompatible types
  --> src/main.rs:7:14
   |
7  |       for i in match x {
   |  ______________^
8  | |         None => 1..5,
9  | |         Some(x) => iter::repeat(x).take(5),
   | |                    ----------------------- match arm with an incompatible type
10 | |     } {
   | |_____^ expected struct `std::ops::Range`, found struct `std::iter::Take`
   |
   = note: expected type `std::ops::Range<{integer}>`
              found type `std::iter::Take<std::iter::Repeat<i64>>`

Esto tiene mucho sentido, por supuesto, pero realmente me gustaría elegir mi iterador en función de una condición, ya que el código en el bucle for no es trivial y copiar y pegar todo eso solo para cambiar la selección del iterador sería bastante feo e inmantenible.

Intenté usarlo as Iterator<Item = i64>en ambos brazos, pero eso me da un error sobre los tipos sin tamaño porque es un objeto de rasgo. ¿Existe una manera fácil de hacer esto?

Por supuesto, podría usarlo .collect()ya que devuelven el mismo tipo e iterar sobre ese vector. Lo cual es una buena solución rápida, pero para listas grandes parece un poco excesiva.

Linear avatar Apr 21 '15 07:04 Linear
Aceptado

La solución más sencilla es utilizar un objeto de rasgo :

use std::iter;

fn main() {
    let mut a;
    let mut b;

    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter: &mut dyn Iterator<Item = i64> = match x {
        None => {
            a = 1..5;
            &mut a
        }
        Some(x) => {
            b = iter::repeat(x).take(5);
            &mut b
        }
    };

    for i in iter {
        println!("{}", i);
    }
}

La principal desventaja de esta solución es que debe asignar espacio de pila para cada tipo de concreto que tenga. Esto también significa variables para cada tipo. Lo bueno es que solo es necesario inicializar el tipo utilizado.

La misma idea, pero que requiere asignación de montón, es utilizar objetos de rasgo en caja :

use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter: Box<dyn Iterator<Item = i64>> = match x {
        None => Box::new(1..5),
        Some(x) => Box::new(iter::repeat(x).take(5)),
    };

    for i in iter {
        println!("{}", i);
    }
}

Esto es especialmente útil cuando desea devolver el iterador de una función . El espacio de pila tomado es un puntero único y solo se asignará el espacio de pila necesario.

También puede utilizar una enumeración para cada posible iterador concreto .

Shepmaster avatar Apr 21 '2015 00:04 Shepmaster

Cualquiera de las cajas proporciona el Eithertipo. Si ambas mitades Eitherson iteradores, entonces también lo es Either:

extern crate either;

use either::Either;
use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter = match x {
        None => Either::Left(1..5),
        Some(x) => Either::Right(iter::repeat(x).take(5)),
    };

    for i in iter {
        println!("{}", i);
    }
}

Como en una respuesta anterior , esto todavía requiere espacio de pila para cada tipo concreto que tenga. Sin embargo, no necesita variables individuales para cada valor concreto.

Este tipo también se puede devolver desde una función , a diferencia de las referencias a objetos de rasgo. En comparación con los objetos de rasgo en caja, siempre utilizará un tamaño fijo en la pila, independientemente del tipo concreto que se haya elegido.

También encontrará este tipo (o equivalente semántico) en otros lugares, comofutures::Either

Shepmaster avatar May 06 '2018 20:05 Shepmaster

Personalmente, en lugar de utilizar Either, prefiero crear una serie de Option<Iterator>valores que se encadenan. Algo como esto:

patio de juegos

use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    for i in pick(x) {
        println!("{}", i);
    }
}

fn pick(opt_x: Option<i64>) -> impl Iterator<Item = i64> {
    let iter_a = if let None = opt_x {
        Some(1..5)  
    } else {
        None
    };

    let iter_b = if let Some(x) = opt_x {
        Some(iter::repeat(x).take(5))
    } else {
        None
    };

    iter_a.into_iter().flatten().chain(iter_b.into_iter().flatten())
}

Es un poco menos obvio que usar Either, pero evita otra caja y, a veces, funciona de manera bastante elegante.

Niko Matsakis avatar Aug 28 '2018 18:08 Niko Matsakis