【问题标题】:How to iterate through two IEnumerables simultaneously?如何同时遍历两个 IEnumerables?
【发布时间】:2011-02-12 21:31:37
【问题描述】:

我有两个枚举:IEnumerable<A> list1IEnumerable<B> list2。我想同时遍历它们,例如:

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

如果它们不包含相同数量的元素,则应抛出异常。

最好的方法是什么?

【问题讨论】:

  • 请也为 Java 提供答案。
  • @Thilo - 为此提出一个单独的问题,因为答案可能非常不同。不要忘记先搜索以防已被询问。
  • @drachenstern 您不能使用 for 循环或知道 IEnumerable 的计数(至少,在没有首先遍历整个事物的情况下),因为它不是一个列表。可能他的可枚举项枚举起来很昂贵,所以他只想做一次。
  • @ChrisF - 由于 List 实现了 IEnumerable,如果 OP 要求提供 List 解决方案,并且提供了 IEnumerable 解决方案,那么这将适用。然而,反之则不成立。 IEnumerable(在 OP 中)没有实现 List。

标签: c# iteration ienumerable


【解决方案1】:

您想要Zip LINQ 运算符之类的东西 - 但 .NET 4 中的版本总是在任一序列完成时截断。

MoreLINQ implementation 有一个 EquiZip 方法,它会抛出一个 InvalidOperationException

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

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

【讨论】:

  • 根据 Apache 许可证 2.0 版获得许可
  • MoreLINQ 源已移至Github
  • @bdrajer:以后请随时自行编辑帖子以使链接正常工作......现在将修复它。
【解决方案2】:

这是这个操作的一个实现,通常称为 Zip:

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

如果只有一个序列用完值,要使其抛出异常,请更改 while 循环:

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

【讨论】:

  • 我从来没有听说过这种Zip算法,它解决了什么类型的问题?清楚地看到它在 4.0 中得到直接支持,这是一个众所周知的问题,即使我自己从来没有需要它。
  • 它允许您并排枚举两个集合。您可以分别链接它们,a.Zip(b).Zip(c),但它会给您 x.Value1.Value1、x.Value1.Value2 和 x.Value2。想想这个名字,一个“拉链”,就像裤子一样。
【解决方案3】:

简而言之,该语言没有提供干净的方法来做到这一点。枚举被设计为一次对一个可枚举对象进行。你可以很容易地模仿 foreach 为你做的事情:

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

如果它们的长度不同,该怎么办取决于您。也许在 while 循环完成后找出哪个元素仍然有元素并继续使用那个元素,如果它们应该是相同的长度则抛出异常,等等。

【讨论】:

    【解决方案4】:

    在 .NET 4 中,您可以在 IEnumerable&lt;T&gt; 上使用 .Zip 扩展方法

    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
    }
    

    但是,它不会出现不等长。不过,您始终可以对其进行测试。

    【讨论】:

      【解决方案5】:

      使用 IEnumerable.GetEnumerator,这样您就可以在枚举中移动。请注意,这可能会有一些非常讨厌的行为,您必须小心。如果你想让它工作,就用这个,如果你想拥有可维护的代码,使用两个 foreach。

      如果您要通过代码多次使用它,您可以创建一个包装类或使用一个库(如 Jon Skeet 建议的那样)以更通用的方式处理此功能。

      我建议的代码:

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

      【讨论】:

      • 我认为这包含一个微妙的错误。如果 firstEnum 比 secondEnum 多一个项,则不会抛出异常。
      • 不要忘记配置枚举器。并且永远不要调用重置。在大多数枚举器中,它会引发异常。包含重置是为了与 COM 枚举器兼容。
      • 注意必须说“if (first is IDisposable) ((IDisposable)first).Dispose();
      • 通过将“&&”更改为“&”,如果任一集合包含的项目比另一个多,现在将无法引发错误。问题是 MoveNext 的返回值在两个地方被用于做两件事,但是对 MoveNext 的每次调用都可能改变它的值。这就是为什么其他解决方案会缓存从 MoveNext 返回的值,即使它看起来像一个冗长、无用的临时变量。
      • @jpabluz:首先:您宁愿在运行时再次启动编译器而不是插入强制转换?请注意,通过使这些动态化,您这个程序片段中的每一个操作也都动态化了。 第二个也不起作用。动态不会调度显式实现。我已经多次描述了如何正确编写代码;你为什么拒绝做正确的事?
      【解决方案6】:
      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
          }
      }
      

      【讨论】:

      • 别忘了处理你的枚举数。
      • @Eric Lippert - 哦,是的!谢谢!
      • 如果 list2 的项目比 list1 多,这会抛出吗?
      • @drachenstern - 我想是的。在这种情况下,moveNext1 为假,moveNext2 为真。由于这些值不相等,它会抛出异常。
      • 哦天哪,我今天早上看不懂...今天能写出任何代码真是个奇迹...(嗯,实际上这可以解释一些事情,例如因为我今天早上的总 LoC 贡献大约是 6...) ~ 我可以先去睡觉明天再试吗?
      【解决方案7】:

      使用Zip 函数类似

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

      虽然这不会引发异常...

      【讨论】:

        【解决方案8】:

        你可以这样做。

        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# 没有 foreach 可以按照您的意愿进行操作(我知道)。

        【讨论】:

        • 别忘了处理枚举数。
        • IEnumerator 不是一次性的。您正在考虑一次性的通用版本 IEnumerator。以上是有效的。
        • 但是您在野外遇到的IEnumerator 实例中有99% 将是IEnumerator&lt;T&gt; 实例,并实现了IEnumerator 以实现向后兼容性。 OP 提出的用例似乎需要延迟计算 IEnumerable&lt;T&gt; 实例,因此代码应该调用 a.GetEnumerator()b.GetEnumerator() 返回的对象的 .Dispose() 方法,如果它们实际上实现了 IDisposable。或者只使用泛型,这样您就可以使用 using 块并获得类型安全作为奖励。
        • 没错。当您将其视为 IEnumerator 时,处置实现 IDisposable 的对象的必要性并不会神奇地消失!
        猜你喜欢
        • 2011-01-30
        • 2011-08-03
        • 1970-01-01
        • 2023-02-02
        • 2018-12-10
        • 1970-01-01
        • 1970-01-01
        • 2021-12-30
        • 1970-01-01
        相关资源
        最近更新 更多