【问题标题】:Looping through 2 lists at once一次循环遍历 2 个列表
【发布时间】:2008-10-28 06:51:50
【问题描述】:

我有两个长度相同的列表,是否可以一次循环遍历这两个列表?

我正在寻找正确的语法来执行以下操作

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

你认为这在 C# 中是可能的吗?如果是,与此等效的 lambda 表达式是什么?

【问题讨论】:

  • 你想连接每个列表的对应元素吗?还是您想要列表的笛卡尔积?
  • @Vincent:因为他提到的长度相同,我猜是第一个。
  • 两者都不是。但这应该没关系吧?
  • 怎么可能两者都不是?您想要 ListA 和 ListB 中的第一个,然后 ListA 和 ListB 中的第二个,等等?

标签: c# list


【解决方案1】:

[编辑]:澄清;这在通用 LINQ / IEnumerable<T> 上下文中很有用,您不能使用索引器,因为 a: 它不存在于可枚举中,并且 b: 你不能保证您可以多次读取数据。由于 OP 提到了 lambda,因此 LINQ 可能不会太远(是的,我确实意识到 LINQ 和 lambda 并不完全相同)。

听起来您需要缺少的Zip 运算符;你可以欺骗它:

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

【讨论】:

  • 很好,但有理由让它成为扩展方法吗?
  • 它使它更可用 - 即点击“。”它看起来,而不是必须了解 SomeUtilityClass.Zip(...);它也非常适合 IEnumerable 的其他扩展方法,因此我对它作为扩展方法感到满意。
  • KeyValuePair 的替代方案当然是采用 Func
  • peterchen:为什么您希望能够在 LINQ 查询中使用这个? :)
  • 返回 KeyValuePair 是更好的方法吗?对于 3 个列表,这怎么可能?而且我认为,在这个问题上使用 KeyValuePair 是不合适的,因为“Key”和“Value”失去了意义。
【解决方案2】:

在普通的旧 for 循环中执行它会更简单...

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

【讨论】:

  • 不同的是你只需要Zip扩展方法一次,你可以永远重复使用它。此外,它适用于任何序列,而不仅仅是列表。
  • 我被 Jon Skeet 羞辱了 :(
【解决方案3】:

你可以明确地做到这一点。

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

至少这个(或类似的东西)是编译器为 foreach 循环所做的。不过我还没有测试过,我猜枚举器缺少一些模板参数。

只需从 List 和 IEnumerator-Interface 中查找 GetEnumerator()。

【讨论】:

  • 你没有在 ListBEnum 上使用过 MoveNext - 你绝对应该使用“using”;不妨看看 Zip 方法。
  • 这不是一个坏方法,他只需要将ListBEnum.MoveNext 移动到while 循环中。它也比 Zip 方法更通用。
【解决方案4】:

我建议使用普通的旧 for 循环,但您应该考虑不同的数组长度。 所以

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

可以变成

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

甚至进入

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

【讨论】:

    【解决方案5】:

    现代答案

    LINQ 现在有一个built-in Zip method,因此您无需创建自己的。结果序列与最短输入一样长。 Zip 目前(从 .NET Core 3.0 开始)有 2 个重载。更简单的返回一个元组序列。它让我们可以生成一些非常简洁的代码,接近原始请求:

    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
    

    或者,同样的事情,但没有元组解包:

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

    另一个重载(出现在 Core 3.0 之前)采用映射函数,让您可以更好地控制结果。此示例返回一个字符串序列,但您可以返回一个任意序列(例如一些自定义类)。

    var numbersAndWords = numbers.Zip(words, (number, word) => $"{number}, {word}");
    
    foreach (string item in numbersAndWords)
        Console.WriteLine(item);
    

    如果您在常规对象上使用 LINQ(而不是使用它来生成 SQL),您还可以查看MoreLINQ,它提供了几种压缩方法。这些方法中的每一个都需要最多 4 个输入序列,而不仅仅是 2 个:

    • EquiZip - 如果输入序列的长度不同,则会引发异常。

    • ZipLongest - 结果序列将始终与最长的输入序列一样长,其中每个较短的序列元素类型的默认值用于填充。

    • ZipShortest - 结果序列与最短输入序列一样短。

    请参阅他们的examples 和/或tests 了解用法。似乎 MoreLINQ 的压缩比普通的 LINQ 早,所以如果你坚持使用旧版本的 .NET,它可能是一个不错的选择。

    【讨论】:

      【解决方案6】:

      我遇到了同样的问题,但是使用了包含列表的对象列表.. 就其价值而言,这可能会对遇到同样问题的人有所帮助。

      由于 IndexOf 为 O(n),因此它的运行时间不是很好,但当时我处理的内部 foreach 循环比本示例中的要多得多,所以我不想处理处理迭代器变量。

      在这样的时候,我非常想念 PHP 的 foreach($arrayList as $key => $value) 表示法......也许我在 C# 中遗漏了一些东西,必须有一种方法可以在 O( c) 时间! (遗憾的是这篇文章说不: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? }
      

      【讨论】:

        【解决方案7】:

        我有一个小函数可以帮助我遍历这两个列表对象。 schema 是 SqlData 类型,它是一个包含三个属性的类。 data 是一个包含动态类型值的列表。首先,我遍历架构集合,而不是使用项目的索引来遍历数据对象。

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

        【讨论】:

          【解决方案8】:

          Senthil Kumar's tech blog,有一系列涵盖(Python)Itertools for C#的实现,包括itertools.izip

          Itertools for C# - Cycle and Zip 开始,您可以为任意数量的iterables(不仅仅是List&LT;T&GT;)提供解决方案。请注意,Zip 在每次迭代中都会产生 Array

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

          }

          代码获取所有可迭代对象的枚举器,将所有枚举器向前移动,将它们的当前值累积到一个数组中并生成该数组。它会这样做,直到任何一个枚举器用完元素为止。

          【讨论】:

          • 好主意;不过,关于实施的两个想法。首先,他没有处理枚举器(主要是异常问题)。但更重要的是,重复使用数组是有风险的:如果我在此调用 .ToArray()/.ToList(),我将有 30 次对同一个数组的引用,并且无法获得早期数据。
          • 我同意,但请注意,每个产量都会创建一个新数组(可以替换为 List 或其他一些 IEnumerable )。
          • 哦,对了!我的错。我以为 T[] 在外面。代码盲区。
          猜你喜欢
          • 2014-06-26
          • 1970-01-01
          • 2017-04-12
          • 2015-10-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-06-22
          相关资源
          最近更新 更多