2010-08-19 15 views
9

Contexto: C# 3.0, .Net 3.5
Supongamos que tengo un método que genera números aleatorios (siempre):Obtener próximos N elementos de enumerable

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

necesito grupo esos números en grupos de 10, entonces me gustaría algo como:

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

He definido el método Slice, pero creo que debería haber 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? Quizás Linq?

Nota: el ejemplo anterior es una simplificación, en mi programa tengo un iterador que escanea la matriz dada de forma no lineal.

EDITAR: Por qué Skip + Take no es bueno.

eficacia 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 generará exactamente 40 números y los dividirá en 4 grupos por 10.

+0

¿Por qué quiere enumerarlo? Simplemente pase la cantidad de números aleatorios que desea generar y luego devuelva una colección de números aleatorios. – epitka

+0

¿Estoy en lo cierto al decir que el código 'RandomNumberGenerator' que publicó no es el código real que está usando? Porque me parece que solo debería devolver un valor único, en lugar de enumerarlo para siempre (para eso necesitaría usar una construcción de bucle). –

+1

@ user93422, ¿no sería más fácil crear una clase de contexto de generador y proporcionar un 'GetNext()' y 'GetNext (int count)' en lugar de intentar dividir una enumeración infinita? Este es el diseño que usa la clase Random y es una manera limpia de lograr lo que busca. –

Respuesta

6

He hecho algo similar. Pero me gustaría que sea más simple:

//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 los tuyos son defectuosos. Usted devuelve la misma matriz para todos sus trozos/rebanadas, por lo que solo el último trozo/porción que tome tendrá 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 se ha señalado, no va a funcionar en las secuencias infinitas y será notablemente inferior 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()); 
} 
+0

bien, supongo que usar array en lugar de list es un ejemplo de optimización prematura. Por cierto, ¿por qué usas '(++ i% size == 0)' en lugar de 'curr.Length == size'? también su 'curr = new List ()' debe ser 'curr = new List (tamaño)' –

+0

He actualizado mi pregunta con su ejemplo. –

+0

Sí, estaba reutilizando la misma matriz (funcionó para mi caso, pero podría ser un error en la mayoría de los casos). –

11

¿Son Skip y Take de alguna utilidad para usted?

Use una combinación de los dos en un bucle para obtener lo que desea.

Así,

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

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

+0

+1 para el más rápido en el sorteo ... – AllenG

+0

No quiero sobrecargar Skip/Take, he editado la pregunta. –

+0

No creo que esto funcione como piensas que será con un generador. –

0

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

1

Usted podría utilice los métodos Skip y Take con cualquier objeto Enumerable.

por tu 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); 
} 
+0

seguirá siendo enumerable para que las rebanadas se omitan. Me gustaría evitar carreras en seco (el rendimiento es una excusa oficial, pero en realidad se siente mal). También su ejemplo enumera T's no T [] como debería (pero entiendo el punto). –

0

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

Un posible mejor enfoque es simplemente usar la extensión Linq Take(). No creo que necesite usar Skip() con un generador.

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

Nota: esto no era realmente correcto, 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 enrollé una vez. 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 se nota que estoy añadiendo la First() a la lista cada vez, pero dado que el enumerable que se pasa en es el generador de RandomNumberGenerator() la el resultado es diferente cada vez

Así que de nuevo con un generador que usa Skip() no es necesario ya que el resultado será diferente. Looping sobre un 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.

+0

Eso es lo que pensé, pero no pude construir un método real usando esa idea. ¿Te importa producir un fragmento? –

+0

Espero que ayude al menos algo. Probablemente debería mejorar los generadores yo mismo. –

+1

@Sean: su función 'RandomNumberGenerator' devuelve un único valor. Tendría que ponerlo en un bucle: 'while (true) yield return random.Next();' –

2

Veamos si necesita la complejidad de Slice. Si el número aleatorio genera no tiene 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 se vuelve a sembrar a sí mismo con un valor específico cada vez que se itera, esto no funcionará. Simplemente obtendrá 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.

+0

'yield result.ToArray()' clona los datos en una nueva matriz, por lo que llamar a 'Clear()' en la lista no afectará a la instancia devuelta de la matriz. –

+0

@ user93422: Sí, lo noté brevemente después de actualizar mi publicación. He corregido mi error. – LBushkin

5

El uso de Skip y Take sería una muy mala idea. Llamar al Skip en una colección indexada puede estar bien, pero llamarlo en cualquier IEnumerable<T> arbitrario puede dar como resultado una enumeración sobre el número de elementos omitidos, lo que significa que si lo llama repetidamente está enumerando sobre la secuencia una orden de magnitud más veces de las que necesita para ser.

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

Creo que su método Slice es tan bueno como se pone. Iba a sugerir un enfoque diferente que proporcionaría la ejecución diferida y obviaría la asignación de matriz intermedia, pero ese es un juego peligroso (es decir, si intentas algo como ToList en una implementación resultante de IEnumerable<T>, sin enumerar las colecciones internas , terminarás en un ciclo 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.)

+0

He cambiado la implementación para usar foreach en su lugar, eso debería ocuparme de eliminar. buen punto aunque +1 –

+0

'yield return result.ToArray();' asegura que cada fragmento devuelto es un objeto-matriz único. –

+0

@ user93422: Sí, veo que se nos ocurrió la misma solución para ese problema. Sin embargo, su código original devolvió la misma matriz, como usted mismo reconoció. –

0

que habían cometido algunos errores en mi respuesta original, pero algunos de los puntos todavía estar. Omitir() y Tomar() no van a funcionar igual con un generador que con una lista. Looping sobre un IEnumerable no siempre es libre de efectos secundarios. De todos modos aquí está mi opinión sobre cómo obtener una lista de 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; 
    } 
0

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

Teniendo en cuenta que la clase, podríamos aplicar la rebanada con

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

Eso funciona en este caso, pero StickyEnumerable<T> es generalmente una clase peligrosa de tener si el código de consumo no lo espera. 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 
0

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 sólo desea que los trozos devueltos , no hacer nada con ellos, use chunks = Chunk(ints, 2, t => t).Lo que realmente me gustaría es tener que tener t=>t como acción predeterminada, pero aún no he descubierto cómo hacerlo.

Cuestiones relacionadas