Itere dos listas o matrices con una instrucción ForEach en C #


142

Esto solo para conocimiento general:

Si tengo dos, digamos, Lista , y quiero iterar ambos con el mismo bucle foreach, ¿podemos hacer eso?

Editar

Solo para aclarar, quería hacer esto:

List<String> listA = new List<string> { "string", "string" };
List<String> listB = new List<string> { "string", "string" };

for(int i = 0; i < listA.Count; i++)
    listB[i] = listA[i];

Pero con un foreach =)


10
La palabra importante aquí es "zip".
Mark Byers

3
¿Quieres iterar dos listas en paralelo ? ¿O quieres iterar primero una lista y luego la otra (con una sola declaración)?
Pavel Minaev

Creo que tu camino se ve mejor que zip
Alexander

Respuestas:


273

Esto se conoce como una operación Zip y será compatible con .NET 4.

Con eso, podrías escribir algo como:

var numbers = new [] { 1, 2, 3, 4 };
var words = new [] { "one", "two", "three", "four" };

var numbersAndWords = numbers.Zip(words, (n, w) => new { Number = n, Word = w });
foreach(var nw in numbersAndWords)
{
    Console.WriteLine(nw.Number + nw.Word);
}

Como alternativa al tipo anónimo con los campos con nombre, también puede ahorrar en llaves usando una Tupla y su Tupla estática.

foreach (var nw in numbers.Zip(words, Tuple.Create)) 
{
    Console.WriteLine(nw.Item1 + nw.Item2);
}


2
No sabía nada sobre esas operaciones Zip, haré una pequeña investigación sobre ese tema. ¡Gracias!
Hugo

44
@Hugo: es una construcción estándar en la programación funcional :)
Mark Seemann

También necesitará usar System.Linq;
Jahmic

55
Desde C # 7, también puede usar un ValueTuple (consulte stackoverflow.com/a/45617748 ) en lugar de tipos anónimos o Tuple.Create. Es decir foreach ((var number, var word) in numbers.Zip(words, (n, w) => (n, w))) { ... }.
Erlend Graff

14

Si no desea esperar .NET 4.0, puede implementar su propio Zipmétodo. Lo siguiente funciona con .NET 2.0. Puede ajustar la implementación según cómo desee manejar el caso en el que las dos enumeraciones (o listas) tienen longitudes diferentes; este continúa hasta el final de la enumeración más larga, devolviendo los valores predeterminados para los elementos faltantes de la enumeración más corta.

static IEnumerable<KeyValuePair<T, U>> Zip<T, U>(IEnumerable<T> first, IEnumerable<U> second)
{
    IEnumerator<T> firstEnumerator = first.GetEnumerator();
    IEnumerator<U> secondEnumerator = second.GetEnumerator();

    while (firstEnumerator.MoveNext())
    {
        if (secondEnumerator.MoveNext())
        {
            yield return new KeyValuePair<T, U>(firstEnumerator.Current, secondEnumerator.Current);
        }
        else
        {
            yield return new KeyValuePair<T, U>(firstEnumerator.Current, default(U));
        }
    }
    while (secondEnumerator.MoveNext())
    {
        yield return new KeyValuePair<T, U>(default(T), secondEnumerator.Current);
    }
}

static void Test()
{
    IList<string> names = new string[] { "one", "two", "three" };
    IList<int> ids = new int[] { 1, 2, 3, 4 };

    foreach (KeyValuePair<string, int> keyValuePair in ParallelEnumerate(names, ids))
    {
        Console.WriteLine(keyValuePair.Key ?? "<null>" + " - " + keyValuePair.Value.ToString());
    }
}

1
Buen método! :). Puede hacer algunos ajustes para usar la misma firma que el método Zip .NET 4 msdn.microsoft.com/en-us/library/dd267698.aspx y devolver resultSelector (primero, segundo) en lugar de un KVP.
Martín Coll

Tenga en cuenta que este método no dispone sus enumeradores, lo que podría convertirse en un problema, por ejemplo, si se usa con enumerables sobre las líneas de archivos abiertos.
Lii

11

Puede usar Union o Concat, el primero elimina duplicados, el segundo no

foreach (var item in List1.Union(List1))
{
   //TODO: Real code goes here
}

foreach (var item in List1.Concat(List1))
{
   //TODO: Real code goes here
}

Otro problema con el uso de una Unión es que puede descartar instancias si evalúan como iguales. Eso no siempre es lo que quieres.
Mark Seemann el

1
Me resistente que su intención era utilizar las colecciones con el mismo tipo,
albertein

@ Mark Seemann, ya lo señalé, él también podría usar Concat
albertein

Al igual que Union, Concat solo funciona si ambas listas son del mismo tipo. No se puede decir si esto es lo que necesita el OP o no, aunque ...
Marcos Seemann

Esto crea una nueva lista que contiene todos los elementos. Esto es un desperdicio de memoria. Utilice el Linq Concat en su lugar.
Drew Noakes

3

Aquí hay un método de extensión IEnumerable <> personalizado que se puede usar para recorrer dos listas simultáneamente.

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
    public static class LinqCombinedSort
    {
        public static void Test()
        {
            var a = new[] {'a', 'b', 'c', 'd', 'e', 'f'};
            var b = new[] {3, 2, 1, 6, 5, 4};

            var sorted = from ab in a.Combine(b)
                         orderby ab.Second
                         select ab.First;

            foreach(char c in sorted)
            {
                Console.WriteLine(c);
            }
        }

        public static IEnumerable<Pair<TFirst, TSecond>> Combine<TFirst, TSecond>(this IEnumerable<TFirst> s1, IEnumerable<TSecond> s2)
        {
            using (var e1 = s1.GetEnumerator())
            using (var e2 = s2.GetEnumerator())
            {
                while (e1.MoveNext() && e2.MoveNext())
                {
                    yield return new Pair<TFirst, TSecond>(e1.Current, e2.Current);
                }
            }

        }


    }
    public class Pair<TFirst, TSecond>
    {
        private readonly TFirst _first;
        private readonly TSecond _second;
        private int _hashCode;

        public Pair(TFirst first, TSecond second)
        {
            _first = first;
            _second = second;
        }

        public TFirst First
        {
            get
            {
                return _first;
            }
        }

        public TSecond Second
        {
            get
            {
                return _second;
            }
        }

        public override int GetHashCode()
        {
            if (_hashCode == 0)
            {
                _hashCode = (ReferenceEquals(_first, null) ? 213 : _first.GetHashCode())*37 +
                            (ReferenceEquals(_second, null) ? 213 : _second.GetHashCode());
            }
            return _hashCode;
        }

        public override bool Equals(object obj)
        {
            var other = obj as Pair<TFirst, TSecond>;
            if (other == null)
            {
                return false;
            }
            return Equals(_first, other._first) && Equals(_second, other._second);
        }
    }

}

3

Desde C # 7, puedes usar Tuples ...

int[] nums = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three", "four" };

foreach (var tuple in nums.Zip(words, (x, y) => (x, y)))
{
    Console.WriteLine($"{tuple.Item1}: {tuple.Item2}");
}

// or...
foreach (var tuple in nums.Zip(words, (x, y) => (Num: x, Word: y)))
{
    Console.WriteLine($"{tuple.Num}: {tuple.Word}");
}

1
¿Qué sucede cuando las dos listas no tienen la misma longitud en esta situación?
John August

Con el (x, y) => (x, y)podemos usar nombre tuple.xy tuple.yque es elegante. Entonces, la segunda forma también podría ser(Num, Word) => (Num, Word)
guiones el

2
@JohnAugust Termina después de atravesar la secuencia más corta. De documentos: "Si las secuencias no tienen el mismo número de elementos, el método fusiona secuencias hasta que llega al final de uno de ellos. Por ejemplo, si una secuencia tiene tres elementos y la otra tiene cuatro, la secuencia resultante tener solo tres elementos ".
gregsmi

0

No, tendrías que usar un bucle for para eso.

for (int i = 0; i < lst1.Count; i++)
{
    //lst1[i]...
    //lst2[i]...
}

No puedes hacer algo como

foreach (var objCurrent1 int lst1, var objCurrent2 in lst2)
{
    //...
}

¿Qué pasa si tienen recuentos diferentes?
Drew Noakes

Entonces, un foreach que aceptaría una lista arbitraria de enumerables no funcionaría tan bien, por lo que todo sería inútil.
Maximilian Mayerl

0

Si desea un elemento con el correspondiente, puede hacer

Enumerable.Range(0, List1.Count).All(x => List1[x] == List2[x]);

Eso devolverá verdadero si cada elemento es igual al correspondiente en la segunda lista

Si eso es casi pero no exactamente lo que quieres, sería útil si elaboraras más.


0

Este método funcionaría para una implementación de lista y podría implementarse como un método de extensión.

public void TestMethod()
{
    var first = new List<int> {1, 2, 3, 4, 5};
    var second = new List<string> {"One", "Two", "Three", "Four", "Five"};

    foreach(var value in this.Zip(first, second, (x, y) => new {Number = x, Text = y}))
    {
        Console.WriteLine("{0} - {1}",value.Number, value.Text);
    }
}

public IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(List<TFirst> first, List<TSecond> second, Func<TFirst, TSecond, TResult> selector)
{
    if (first.Count != second.Count)
        throw new Exception();  

    for(var i = 0; i < first.Count; i++)
    {
        yield return selector.Invoke(first[i], second[i]);
    }
}

0

También podría simplemente usar una variable entera local si las listas tienen la misma longitud:

List<classA> listA = fillListA();
List<classB> listB = fillListB();

var i = 0;
foreach(var itemA in listA)
{
    Console.WriteLine(itemA  + listB[i++]);
}

-1

También puede hacer lo siguiente:

var i = 0;
foreach (var itemA in listA)
{
  Console.WriteLine(itemA + listB[i++]);
}

Nota: la longitud de listAdebe ser igual a listB.


-3

Entiendo / espero que las listas tengan la misma longitud: No, su única apuesta es ir con un estándar antiguo para bucle.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.