Pregunta

Contexto: C # 3.0, .Net 3.5
Supongamos que tengo un método que genera números aleatorios (siempre):

private static IEnumerable<int> RandomNumberGenerator() {
    while (true) yield return GenerateRandomNumber(0, 100);
}

Necesito grupo de esos números en grupos de 10, así que me gustaría algo como:

foreach (IEnumerable<int> group in RandomNumberGenerator().Slice(10)) {
    Assert.That(group.Count() == 10);
}

He definido el método de la rebanada, pero siento que debería ser uno ya definido. Aquí es mi método de la rebanada, apenas para la referencia:

    private static IEnumerable<T[]> Slice<T>(IEnumerable<T> enumerable, int size) {
        var result = new List<T>(size);
        foreach (var item in enumerable) {
            result.Add(item);
            if (result.Count == size) {
                yield return result.ToArray();
                result.Clear();
            }
        }
    }

Pregunta: ¿hay una manera más fácil de lograr lo que estoy tratando de hacer? Tal vez LINQ?

Nota:. Ejemplo anterior es una simplificación, en mi programa tengo un iterador que exploraciones dan matriz de una manera no lineal

EDIT: ¿Por qué Skip + Take no es bueno.

Efectivamente lo que quiero es:

var group1 = RandomNumberGenerator().Skip(0).Take(10);
var group2 = RandomNumberGenerator().Skip(10).Take(10);
var group3 = RandomNumberGenerator().Skip(20).Take(10);
var group4 = RandomNumberGenerator().Skip(30).Take(10);

sin la sobrecarga de la regeneración de número (10 + 20 + 30 + 40) veces. Necesito una solución que va a generar exactamente 40 números y romper aquellos en los 4 grupos de 10.

¿Fue útil?

Solución

he hecho algo similar. Pero me gustaría que sea más sencillo:

//Remove "this" if you don't want it to be a extension method
public static IEnumerable<IList<T>> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new List<T>(size);

    foreach (var x in xs)
    {
        curr.Add(x);

        if (curr.Count == size)
        {
            yield return curr;
            curr = new List<T>(size);
        }
    }
}

creo que la suya están viciados. Volverá la misma matriz para todas sus trozos / rebanadas de modo que sólo el trozo última / rebanada llevará tendría los datos correctos.

Adición: versión Matriz:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new T[size];

    int i = 0;

    foreach (var x in xs)
    {
        curr[i % size] = x;

        if (++i % size == 0)
        {
            yield return curr;
            curr = new T[size];
        }
    }
}

Adición: versión Linq (no C # 2.0). Como a cabo en punta, no funcionará en secuencias infinitas y será una gran cantidad más lento que las alternativas:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    return xs.Select((x, i) => new { x, i })
             .GroupBy(xi => xi.i / size, xi => xi.x)
             .Select(g => g.ToArray());
}

Otros consejos

Saltar y Tome de ninguna utilidad para usted?

El uso de una combinación de los dos en un bucle para conseguir lo que quieres.

Por lo tanto,

list.Skip(10).Take(10);

Omite los primeros 10 registros y luego toma la siguiente 10.

El uso de Skip y Take sería una muy mala idea . Llamando Skip en una colección indexada puede estar bien, pero llamar en cualquier IEnumerable<T> arbitraria es susceptible de causar una enumeración sobre el número de elementos omitidos, lo que significa que si usted está llamando repetidamente que está enumerando sobre la secuencia un orden de magnitud más veces de las que necesita ser .

quejan de "optimización prematura" todo lo que quiera; pero eso es simplemente ridículo.

Creo que su método Slice es casi tan bueno como se pone. Yo iba a sugerir un enfoque diferente que proporcionaría la ejecución diferida y obviar la intermedia asignación de matriz, pero que es un juego muy peligroso (es decir, si intenta algo así como ToList en tal resultante aplicación IEnumerable<T>, sin que la enumeración sobre las colecciones interiores , que va a terminar en un bucle sin fin).

(He quitado lo que era originalmente aquí, como mejoras de la OP desde la publicación de la cuestión desde entonces han prestado mis sugerencias aquí redundante.)

.

Ver Vamos si siquiera necesita la complejidad de la rebanada Si el número aleatorio genera es sin estado, asumiría cada llamada a que sería generar números aleatorios únicos, así que quizás esto sería suficiente:

var group1 = RandomNumberGenerator().Take(10);  
var group2 = RandomNumberGenerator().Take(10);  
var group3 = RandomNumberGenerator().Take(10);  
var group4 = RandomNumberGenerator().Take(10);

Cada llamada a Take devuelve un nuevo grupo de 10 números.

Ahora, si su generador de números aleatorios en sí re-semillas con un valor específico cada vez que se itera, esto no funcionará. Simplemente va a obtener los mismos 10 valores para cada grupo. Así que en lugar, se debería utilizar:

var generator  = RandomNumberGenerator();
var group1     = generator.Take(10);  
var group2     = generator.Take(10);  
var group3     = generator.Take(10);  
var group4     = generator.Take(10);

Esto mantiene una instancia del generador para que pueda continuar con la recuperación de los valores sin volver a sembrar el generador.

Se podría utilizar el Saltar y Tome métodos con cualquier objeto Enumerable.

Para su edición:

¿Qué tal una función que toma un número de segmento y un tamaño de rodaja como parámetro?

private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
    return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}

Parece que preferiríamos para una IEnumerable<T> tener un contador de posición fija de manera que podemos hacer

var group1 = items.Take(10);
var group2 = items.Take(10);
var group3 = items.Take(10);
var group4 = items.Take(10);

y obtener rebanadas sucesivas en lugar de obtener los primeros 10 elementos cada vez. Podemos hacer eso con una nueva implementación de IEnumerable<T> que mantiene una instancia de su enumerador y lo devuelve en cada llamada de GetEnumerator:

public class StickyEnumerable<T> : IEnumerable<T>, IDisposable
{
    private IEnumerator<T> innerEnumerator;

    public StickyEnumerable( IEnumerable<T> items )
    {
        innerEnumerator = items.GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return innerEnumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return innerEnumerator;
    }

    public void Dispose()
    {
        if (innerEnumerator != null)
        {
            innerEnumerator.Dispose();
        }
    }
}

Dado que clase, que podría poner en práctica la rebanada con el

public static IEnumerable<IEnumerable<T>> Slices<T>(this IEnumerable<T> items, int size)
{
    using (StickyEnumerable<T> sticky = new StickyEnumerable<T>(items))
    {
        IEnumerable<T> slice;
        do
        {
            slice = sticky.Take(size).ToList();
            yield return slice;
        } while (slice.Count() == size);
    }
    yield break;
}

que funciona en este caso, pero StickyEnumerable<T> es generalmente una clase peligroso tener en torno a si el código que consume no se lo esperaba. Por ejemplo,

using (var sticky = new StickyEnumerable<int>(Enumerable.Range(1, 10)))
{
    var first = sticky.Take(2);
    var second = sticky.Take(2);
    foreach (int i in second)
    {
        Console.WriteLine(i);
    }
    foreach (int i in first)
    {
        Console.WriteLine(i);
    }
}

impresiones

1
2
3
4

en lugar de

3
4
1
2

Tome un vistazo a Take (), TakeWhile () y Skip ()

Creo que el uso de Slice() sería un poco engañoso. Pienso que como un medio para darme un mandril de una matriz en una nueva matriz y que no causa efectos secundarios. En este escenario, cabe realmente mover el enumerables hacia adelante 10.

Un posible enfoque mejor es usar la extensión Take() LINQ. No creo que tendría que utilizar Skip() con un generador.

Editar Dang, he estado tratando de probar este comportamiento con el siguiente código

Nota:. esta es en realidad no era correcta, lo dejo aquí para que otros no caigan en el mismo error

var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);

public static IEnumerable<int> RandomNumberGenerator()
{
    yield return random.Next();
}

pero el Count() para slice es todos los días 1. También probé la ejecución a través de un bucle foreach ya que sé que las extensiones de LINQ son generalmente evaluados con pereza y sólo una vez en bucle. Finalmente me hice el código de abajo en lugar de la Take() y funciona:

public static IEnumerable<int> Slice(this IEnumerable<int> enumerable, int size)
{
    var list = new List<int>();
    foreach (var count in Enumerable.Range(0, size)) list.Add(enumerable.First());
    return list;
}

Si usted nota que estoy añadiendo la First() a la lista cada vez, pero dado que el enumerable que se pasa en el generador es de RandomNumberGenerator() el resultado es diferente cada vez.

Así que de nuevo con no es necesario el uso de un generador Skip() ya que el resultado será diferente. Bucle a través de una IEnumerable no siempre es libre de efectos secundarios.

Editar Voy a dejar la última edición simplemente por lo que no se cae en el mismo error, pero funcionó bien para mí acaba de hacer esto:

var numbers = RandomNumberGenerator();

var slice1 = numbers.Take(10);
var slice2 = numbers.Take(10);

Los dos rebanadas eran diferentes.

Me había cometido algunos errores en mi respuesta original, pero algunos de los puntos siguen en pie. Skip () y Take () no va a funcionar de la misma con un generador como lo haría una lista. Bucle sobre un IEnumerable no siempre es el lado libre de efecto. De todos modos aquí es mi opinión sobre conseguir una lista de las rebanadas.

    public static IEnumerable<int> RandomNumberGenerator()
    {
        while(true) yield return random.Next();
    }

    public static IEnumerable<IEnumerable<int>> Slice(this IEnumerable<int> enumerable, int size, int count)
    {
        var slices = new List<List<int>>();
        foreach (var iteration in Enumerable.Range(0, count)){
            var list = new List<int>();
            list.AddRange(enumerable.Take(size));
            slices.Add(list);
        }
        return slices;
    }

Tengo esta solución para el mismo problema:

int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IEnumerable<IEnumerable<int>> chunks = Chunk(ints, 2, t => t.Dump());
//won't enumerate, so won't do anything unless you force it:
chunks.ToList();

IEnumerable<T> Chunk<T, R>(IEnumerable<R> src, int n, Func<IEnumerable<R>, T> action){
  IEnumerable<R> head;
  IEnumerable<R> tail = src;
  while (tail.Any())
  {
    head = tail.Take(n);
    tail = tail.Skip(n);
    yield return action(head);
  }
}

si lo que desea los trozos devueltos, no hacer nada con ellos, el uso chunks = Chunk(ints, 2, t => t). Lo que realmente me gustaría es tener que tener t=>t como acción por defecto, pero no he encontrado la manera de hacer eso todavía.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top