2010-04-27 18 views
42

tengo que enumerables IEnumerable<A> list1 y IEnumerable<B> list2 y me gustaría iterar a través de ellos a la vez como:¿Cómo iterar a través de dos IEnumerables simultáneamente?

foreach((a, b) in (list1, list2)) { 
    // use a and b 
} 

Si no contienen el mismo número de elementos, una excepción debe ser lanzado.

¿Cuál es la mejor manera de hacerlo?

+0

Respuestas para Java, también, por favor. – Thilo

+0

¿Duplicado de http://stackoverflow.com/questions/981867/is-it-possible-to-iterate-over-two-ienumerable-objects-at-the-same-time? –

+3

@Thilo: haga una pregunta por separado, ya que la respuesta probablemente sea muy diferente. No te olvides de buscar primero en caso de que ya se haya pedido. – ChrisF

Respuesta

24

Aquí es una implementación de esta operación, normalmente denominado Código Postal:

using System; 
using System.Collections.Generic; 

namespace SO2721939 
{ 
    public sealed class ZipEntry<T1, T2> 
    { 
     public ZipEntry(int index, T1 value1, T2 value2) 
     { 
      Index = index; 
      Value1 = value1; 
      Value2 = value2; 
     } 

     public int Index { get; private set; } 
     public T1 Value1 { get; private set; } 
     public T2 Value2 { get; private set; } 
    } 

    public static class EnumerableExtensions 
    { 
     public static IEnumerable<ZipEntry<T1, T2>> Zip<T1, T2>(
      this IEnumerable<T1> collection1, IEnumerable<T2> collection2) 
     { 
      if (collection1 == null) 
       throw new ArgumentNullException("collection1"); 
      if (collection2 == null) 
       throw new ArgumentNullException("collection2"); 

      int index = 0; 
      using (IEnumerator<T1> enumerator1 = collection1.GetEnumerator()) 
      using (IEnumerator<T2> enumerator2 = collection2.GetEnumerator()) 
      { 
       while (enumerator1.MoveNext() && enumerator2.MoveNext()) 
       { 
        yield return new ZipEntry<T1, T2>(
         index, enumerator1.Current, enumerator2.Current); 
        index++; 
       } 
      } 
     } 
    } 

    class Program 
    { 
     static void Main(string[] args) 
     { 
      int[] numbers = new[] { 1, 2, 3, 4, 5 }; 
      string[] names = new[] { "Bob", "Alice", "Mark", "John", "Mary" }; 

      foreach (var entry in numbers.Zip(names)) 
      { 
       Console.Out.WriteLine(entry.Index + ": " 
        + entry.Value1 + "-" + entry.Value2); 
      } 
     } 
    } 
} 

Para que sea una excepción si sólo una de las secuencias se queda sin valores, cambiar el bucle while modo:

while (true) 
{ 
    bool hasNext1 = enumerator1.MoveNext(); 
    bool hasNext2 = enumerator2.MoveNext(); 
    if (hasNext1 != hasNext2) 
     throw new InvalidOperationException("One of the collections ran " + 
      "out of values before the other"); 
    if (!hasNext1) 
     break; 

    yield return new ZipEntry<T1, T2>(
     index, enumerator1.Current, enumerator2.Current); 
    index++; 
} 
+1

Esto es bueno: D – Danvil

+0

Nunca he oído hablar de este algoritmo de Zip utilizado antes, ¿qué tipo de problema resuelve esto? Viendo que es directamente compatible con 4.0, es un problema muy conocido, incluso si nunca lo he necesitado. –

+0

Le permite enumerar más de dos colecciones una al lado de la otra. Puede encadenarlos cada uno, 'a.Zip (b) .Zip (c)', pero le dará x.Value1.Value1, x.Value1.Value2 y x.Value2. Piense en el nombre, una "cremallera", como en los pantalones. –

43

Desea algo como el operador LINQ Zip, pero la versión en .NET 4 siempre se trunca cuando termina una de las secuencias.

El MoreLINQ implementation tiene un método EquiZip que arrojará un InvalidOperationException en su lugar.

var zipped = list1.EquiZip(list2, (a, b) => new { a, b }); 

foreach (var element in zipped) 
{ 
    // use element.a and element.b 
} 
+7

Copyright (c) 2008-9 Jonathan Skeet =) – jpabluz

+0

Licencia bajo la Licencia Apache, Versión 2.0 –

+0

La fuente MoreLINQ se movió a [Github] (https://github.com/morelinq/MoreLINQ/blob/master/MoreLinq/ EquiZip.cs). – bdrajer

3

Vaya con IEnumerable.GetEnumerator, para que pueda moverse por el enumerable. Tenga en cuenta que esto podría tener un comportamiento realmente desagradable, y debe tener cuidado. Si quieres que funcione, sigue con esto, si quieres tener un código de mantenimiento, usa dos foreach.

Puede crear una clase de ajuste o utilizar una biblioteca (como lo sugiere Jon Skeet) para manejar esta funcionalidad de una manera más genérica si va a utilizarla más de una vez a través de su código.

El código para lo que sugieren:

var firstEnum = aIEnumerable.GetEnumerator(); 
var secondEnum = bIEnumerable.GetEnumerator(); 

var firstEnumMoreItems = firstEnum.MoveNext(); 
var secondEnumMoreItems = secondEnum.MoveNext();  

while (firstEnumMoreItems && secondEnumMoreItems) 
{ 
     // Do whatever. 
     firstEnumMoreItems = firstEnum.MoveNext(); 
     secondEnumMoreItems = secondEnum.MoveNext(); 
} 

if (firstEnumMoreItems || secondEnumMoreItems) 
{ 
    Throw new Exception("One Enum is bigger"); 
} 

// IEnumerator does not have a Dispose method, but IEnumerator<T> has. 
if (firstEnum is IDisposable) { ((IDisposable)firstEnum).Dispose(); } 
if (secondEnum is IDisposable) { ((IDisposable)secondEnum).Dispose(); } 
+1

Creo que esto contiene un error sutil. Si firstEnum contiene un elemento más que secondEnum, no se lanzará ninguna excepción. –

+5

No olvides desechar los enumeradores. Y NUNCA SIEMPRE llama a Restablecer. En la mayoría de los enumeradores arroja una excepción. El reinicio se incluyó para la compatibilidad con los enumeradores COM. –

+0

Gracias por los comentarios, aprendí algo nuevo hoy. – jpabluz

14

En resumen, el lenguaje no ofrece ninguna manera limpia de hacer esto. La enumeración fue diseñada para ser hecha sobre un enumerable a la vez. Puede imitar lo que hace foreach para usted con bastante facilidad:

IEnumerator<A> list1enum = list1.GetEnumerator(); 
IEnumerator<B> list2enum = list2.GetEnumerator();  
while(list1enum.MoveNext() && list2enum.MoveNext()) { 
     // list1enum.Current and list2enum.Current point to each current item 
    } 

¿Qué hacer si son de diferente longitud depende de usted. Quizás descubra cuál todavía tiene elementos después de que el ciclo while esté listo y continúe trabajando con ese, ejecute una excepción si deben tener la misma longitud, etc.

+10

Este código tiene errores ya que no elimina los enumeradores. –

1

Puede hacer algo como esto.

IEnumerator enuma = a.GetEnumerator(); 
IEnumerator enumb = b.GetEnumerator(); 
while (enuma.MoveNext() && enumb.MoveNext()) 
{ 
    string vala = enuma.Current as string; 
    string valb = enumb.Current as string; 
} 

C# no tiene ningún engaño que pueda hacerlo como quiera (que yo sepa).

+1

No olvide disponer de los enumeradores. –

+0

IEnumerator no es desechable. Está pensando en la versión genérica IEnumerator que es desechable. Lo anterior es válido. –

+1

Pero el 99% de las instancias de 'IEnumerator' que se encuentran en la naturaleza van a ser instancias' IEnumerator 'con' IEnumerator' implementado para compatibilidad con versiones anteriores. El caso de uso que el OP está proponiendo parece requerir instancias 'IEnumerable ' perezosamente calculadas, por lo que el código debería llamar a los métodos '.Dispose()' de los objetos devueltos por 'a.GetEnumerator()' y 'b.GetEnumerator () 'si realmente implementan' IDisposable'. O simplemente hazlo con genéricos, así que puedes usar un bloque 'using' y obtener type-safety como bonificación. –

1

utilizar la función Zip como

foreach (var entry in list1.Zip(list2, (a,b)=>new {First=a, Second=b}) { 
    // use entry.First und entry.Second 
} 

Esto no lanzar una excepción, aunque ...

3

En .NET 4, puede utilizar el método de extensión .zip en IEnumerable<T>

IEnumerable<int> list1 = Enumerable.Range(0, 100); 
IEnumerable<int> list2 = Enumerable.Range(100, 100); 

foreach (var item in list1.Zip(list2, (a, b) => new { a, b })) 
{ 
    // use item.a and item.b 
} 

Sin embargo, no arrojará longitudes desiguales. Sin embargo, siempre puedes probar eso.

2
using(var enum1 = list1.GetEnumerator()) 
using(var enum2 = list2.GetEnumerator()) 
{ 
    while(true) 
    { 
     bool moveNext1 = enum1.MoveNext(); 
     bool moveNext2 = enum2.MoveNext(); 
     if (moveNext1 != moveNext2) 
      throw new InvalidOperationException(); 
     if (!moveNext1) 
      break; 
     var a = enum1.Current; 
     var b = enum2.Current; 
     // use a and b 
    } 
} 
+2

No olvide disponer de sus enumeradores. –

+0

@Eric Lippert - ¡Oh sí! ¡Gracias! –

+0

¿Esto arrojará si list2 tiene más elementos que list1? – jcolebrand

Cuestiones relacionadas