Ітерація двох списків або масивів одним твердженням ForEach у C #


142

Це лише для загальних знань:

Якщо у мене є два, скажімо, « Список» , і я хочу повторити обидва з одним циклом foreach, чи можемо ми це зробити?

Редагувати

Просто для уточнення я хотів це зробити:

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

Але з передбаченням =)


10
Тут важливе слово - "блискавка".
Марк Байєрс

3
Ви хочете повторити два списки паралельно ? Або ви хочете повторити спочатку один список, а потім другий (з одним твердженням)?
Павло Мінаєв

Я думаю, ваш спосіб виглядає краще, ніж блискавка
Олександр,

Відповіді:


274

Це відомо як операція Zip і буде підтримуватися в .NET 4.

З цим ви зможете написати щось на кшталт:

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

Як альтернатива анонімному типу з названими полями, ви також можете заощадити на дужках, використовуючи Tuple та його статичний помічник Tuple.Create:

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


2
Я нічого не знав про ті операції Zip, я зроблю невелике дослідження на цю тему. Дякую!
Гюго

4
@Hugo: Це стандартна конструкція у функціональному програмуванні :)
Марк Семанн

Вам також знадобиться використовувати System.Linq;
Джаміч

5
Так як C # 7, ви можете також використовувати ValueTuple (див stackoverflow.com/a/45617748 ) замість анонімних типів або Tuple.Create. Тобто foreach ((var number, var word) in numbers.Zip(words, (n, w) => (n, w))) { ... }.
Ерленд Графф

14

Якщо ви не хочете чекати .NET 4.0, ви можете реалізувати власний Zipметод. Далі працює з .NET 2.0. Ви можете налаштувати реалізацію залежно від того, як ви хочете обробити випадок, коли два перерахування (або списки) мають різну довжину; ця триває до кінця більш тривалого перерахування, повертаючи значення за замовчуванням для відсутніх елементів із коротшого перерахування.

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
Гарний метод! :). Ви можете внести кілька коригувань, щоб використовувати той самий підпис, що і .NET 4 Zip метод msdn.microsoft.com/en-us/library/dd267698.aspx та return resultSelector (перший, другий) замість KVP.
Мартін Колл

Зауважте, що цей метод не розпоряджається своїми перелічувачами, що може стати проблемою, наприклад, якщо він використовується із перелічувальними номерами над рядками відкритих файлів.
Лії

11

Ви можете використовувати Union або Concat, колишній видаляє дублікати, пізніше - не

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

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

Іншою проблемою використання Union є те, що він може викинути екземпляри, якщо вони оцінять як рівне. Це не завжди може бути тим, що ви хочете.
Марк Семанн

1
Я твердий, що його намір полягав у використанні колекцій одного типу,
Альбертейн

@Mark Seemann, я вже вказував, що він також може використовувати Concat
Альбертейн

Як і Union, Concat працює лише тоді, коли обидва списки одного типу. Не можу сказати, чи потрібно це ОП чи ні, хоча ...
Марк Семанн

Це створює новий список, який містить усі елементи. Це марно пам'ять. Використовуйте замість цього Linq Concat.
Дрю Ноакс

3

Ось спеціальний метод розширення IEnumerable <>, який можна використовувати для перегляду циклу через два списки одночасно.

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

Оскільки C # 7, ви можете використовувати 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
Що відбувається, коли два списки не мають однакової довжини в цій ситуації?
Джон Серпень

З (x, y) => (x, y)ми можемо використовувати іменовані tuple.xі tuple.yякий є елегантним. Отже, друга форма також могла бути(Num, Word) => (Num, Word)
тире

2
@JohnAugust Закінчується після проходження коротшої послідовності. З документів: "Якщо послідовності не мають однакової кількості елементів, метод об'єднує послідовності, поки не досягне кінця одного з них. Наприклад, якщо одна послідовність має три елементи, а інша - чотири, послідовність результатів буде мають лише три елементи ».
gregsmi

0

Ні, вам доведеться використовувати для цього цикл.

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

Ви не можете зробити щось подібне

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

Що робити, якщо вони мають різні підрахунки?
Дрю Ноакс

Тоді не може працювати і проповідь, яка приймає довільний перелік перелічених номерів, тим самим робить всю справу марною.
Максиміліан Майерл

0

Якщо ви хочете один елемент з відповідним, ви можете це зробити

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

Це повернеться істинним, якщо кожен пункт дорівнює відповідному одному у другому списку

Якщо це майже, але не зовсім те, що ви хочете, це допомогло б, якщо ви допрацювали більше.


0

Цей метод працює для реалізації списку і може бути реалізований як метод розширення.

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

Ви також можете просто використовувати локальну змінну цілого числа, якщо списки мають однакову довжину:

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

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

-1

Ви також можете зробити наступне:

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

Примітка: довжина listAмає бути однаковою listB.


-3

Я розумію / сподіваюсь, що списки мають однакову довжину: Ні, ваша єдина ставка йде зі звичайним старим стандартом для циклу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.