Graviton
Graviton

Reputation: 83326

Looping through 2 lists at once

I have two lists that are of the same length, is it possible to loop through these two lists at once?

I am looking for the correct syntax to do the below

foreach itemA, itemB in ListA, ListB
{
  Console.WriteLine(itemA.ToString()+","+itemB.ToString());
}

do you think this is possible in C#? And if it is, what is the lambda expression equivalent of this?

Upvotes: 25

Views: 21760

Answers (8)

MarredCheese
MarredCheese

Reputation: 20911

Modern Answer

LINQ now has a built-in Zip method, so you don't need to create your own. The resulting sequence is as long as the shortest input. Zip currently (as of .NET Core 3.0) has 2 overloads. The simpler one returns a sequence of tuples. It lets us produce some very terse code that's close to the original request:

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

foreach (var (number, word) in numbers.Zip(words))
    Console.WriteLine($"{number}, {word}");

// 1, one
// 2, two
// 3, three

Or, the same thing but without tuple unpacking:

foreach (var item in numbers.Zip(words))
    Console.WriteLine($"{item.First}, {item.Second}");

The other overload (which appeared earlier than Core 3.0) takes a mapping function, giving you more control over the result. This example returns a sequence of strings, but you could return a sequence of whatever (e.g. some custom class).

var numbersAndWords = numbers.Zip(words, (number, word) => $"{number}, {word}");

foreach (string item in numbersAndWords)
    Console.WriteLine(item);

If you are using LINQ on regular objects (as opposed to using it to generate SQL), you can also check out MoreLINQ, which provides several zipping methods. Each of those methods takes up to 4 input sequences, not just 2:

  • EquiZip - An exception is thrown if the input sequences are of different lengths.

  • ZipLongest - The resulting sequence will always be as long as the longest of input sequences where the default value of each of the shorter sequence element types is used for padding.

  • ZipShortest - The resulting sequence is as short as the shortest input sequence.

See their examples and/or tests for usage. It seems MoreLINQ's zipping came before regular LINQ's, so if you're stuck with an old version of .NET, it might be a good option.

Upvotes: 7

Joy Fernandes
Joy Fernandes

Reputation: 303

I have this small function which helps me to iterate through this two list objects. schema is of type SqlData, which is a class that hold three properties. And data is a list that holds values of dynamic type. First I'm iterating through the schema collection and than using the index of item to iterate through the data object.

public List<SqlData> SqlDataBinding(List<SqlData> schema, List<dynamic> data)
{
    foreach (SqlData item in schema)
    {
        item.Values = data[schema.IndexOf(item)];
    }
    return schema
}

Upvotes: 0

sonjz
sonjz

Reputation: 5090

I had this same problem but using lists of objects with lists inside of them.. for what its worth, this might help someone with the same issue.

The running time of this isn't very good since IndexOf is O(n), but at the time, I'm dealing with a lot more inner-foreach loops than in this example, so I didn't want to deal with handling iterator variables.

At times like this I very much miss PHPs foreach($arrayList as $key => $value) notation... maybe I'm missing something in C#, there's got to be a way to get the index in O(c) time! (sadly this post says no: Getting the array key in a 'foreach' loop)

class Stock {
   string symbol;
   List<decimal> hourlyPrice; // provides a list of 24 decimals
}

// get hourly prices from yesterday and today
List<Stock> stockMondays = Stocks.GetStock("GOOGL,IBM,AAPL", DateTime.Now.AddDay(-1));
List<Stock> stockTuesdays = Stocks.GetStock("GOOGL,IBM,AAPL", DateTime.Now);

try {
    foreach(Stock sMonday in stockMondays) {
        Stock sTuesday = stockTuesday[stockMondays.IndexOf(sMonday)];

        foreach(decimal mondayPrice in sMonday.prices) {
            decimal tuesdayPrice = sTuesday.prices[sMonday.prices.IndexOf(mondayPrice)];
            // do something now
        }

    }
} catch (Exception ex) { // some reason why list counts aren't matching? }

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1064274

[edit]: to clarify; this is useful in the generic LINQ / IEnumerable<T> context, where you can't use an indexer, because a: it doesn't exist on an enumerable, and b: you can't guarantee that you can read the data more than once. Since the OP mentions lambdas, it occurs that LINQ might not be too far away (and yes, I do realise that LINQ and lambdas are not quite the same thing).

It sounds like you need the missing Zip operator; you can spoof it:

static void Main()
{
    int[] left = { 1, 2, 3, 4, 5 };
    string[] right = { "abc", "def", "ghi", "jkl", "mno" };

    // using KeyValuePair<,> approach
    foreach (var item in left.Zip(right))
    {
        Console.WriteLine("{0}/{1}", item.Key, item.Value);
    }

    // using projection approach
    foreach (string item in left.Zip(right,
        (x,y) => string.Format("{0}/{1}", x, y)))
    {
        Console.WriteLine(item);
    }
}

// library code; written once and stuffed away in a util assembly...

// returns each pais as a KeyValuePair<,>
static IEnumerable<KeyValuePair<TLeft,TRight>> Zip<TLeft, TRight>(
    this IEnumerable<TLeft> left, IEnumerable<TRight> right)
{
    return Zip(left, right, (x, y) => new KeyValuePair<TLeft, TRight>(x, y));
}

// accepts a projection from the caller for each pair
static IEnumerable<TResult> Zip<TLeft, TRight, TResult>(
    this IEnumerable<TLeft> left, IEnumerable<TRight> right,
    Func<TLeft, TRight, TResult> selector)
{
    using(IEnumerator<TLeft> leftE = left.GetEnumerator())
    using (IEnumerator<TRight> rightE = right.GetEnumerator())
    {
        while (leftE.MoveNext() && rightE.MoveNext())
        {
            yield return selector(leftE.Current, rightE.Current);
        }
    }
}

Upvotes: 27

PiRX
PiRX

Reputation: 3575

I recommend using plain old for loop, but you should consider different array lengths. So

for(int i=0; i<ListA.Length; i++)
{
    Console.WriteLine(ListA[i].ToString() + ", " + ListB[i].ToString());
}

can turn into

for(int i = 0; i < Math.Min(ListA.Length, ListB.Lenght); i++)
{
    Console.WriteLine(ListA[i].ToString() + ", " + ListB[i].ToString());
}

or even into

    for(int i = 0; i < Math.Max(ListA.Length, ListB.Lenght); i++)
    {
        string valueA = i < ListA.Length ? listA[i].ToString() : "";
        string valueB = i < ListB.Length ? listB[i].ToString() : "";

        Console.WriteLine(valueA+ ", " + valueB);
    }

Upvotes: 2

gimel
gimel

Reputation: 86502

Senthil Kumar's tech blog, has a series covering implementations of (Python) Itertools for C#, including itertools.izip.

From Itertools for C# - Cycle and Zip, you have a solution for any number of iterables (not only List<T>). Note that Zip yields an Array on each iteration:

public static IEnumerable<T[]> Zip<T>(params IEnumerable<T>[] iterables)
{
IEnumerator<T>[] enumerators = Array.ConvertAll(iterables, (iterable) =>   iterable.GetEnumerator());

while (true)
{
   int index = 0;
   T[] values = new T[enumerators.Length];

   foreach (IEnumerator<T> enumerator in enumerators)
   {
       if (!enumerator.MoveNext())
          yield break;

        values[index++] = enumerator.Current;
   }

   yield return values;
}

}

The code gets enumerators for all the iterables, moves all enumerators forward, accumulates their current values into an array and yields the array. It does this until any one of the enumerators runs out of elements.

Upvotes: -1

Tobias Langner
Tobias Langner

Reputation:

You can do it explicit.

IEnumerator ListAEnum = ListA.GetEnumerator();
IEnumerator ListBEnum = ListB.GetEnumerator();

ListBEnum.MoveNext();
while(ListAEnum.MoveNext()==true)
{
  itemA=ListAEnum.getCurrent();
  itemB=ListBEnum.getCurrent();
  Console.WriteLine(itemA.ToString()+","+itemB.ToString());
}

At least this (or something like this) is what the compiler does for a foreach-loop. I haven't tested it though and I guess some template parameters are missing for the enumerators.

Just look up GetEnumerator() from List and the IEnumerator-Interface.

Upvotes: 4

jcelgin
jcelgin

Reputation: 1174

It'll be much simpler to just do it in a plain old for loop instead...

for(int i=0; i<ListA.Length; i++)
{
    Console.WriteLine(ListA[i].ToString() + ", " + ListB[i].ToString());
}

Upvotes: 13

Related Questions