如何同时迭代两个 IEnumerable?

问题描述 投票:0回答:9

我有两个可枚举:

IEnumerable<A> list1
IEnumerable<B> list2
。我想同时迭代它们,例如:

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

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

最好的方法是什么?

c# iteration ienumerable
9个回答
58
投票

37
投票

这是此操作的实现,通常称为 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++;
}

22
投票

简而言之,该语言没有提供干净的方法来做到这一点。枚举被设计为一次对一个枚举进行。您可以轻松模仿 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 循环完成后找出哪一个仍然有元素并继续处理那个元素,如果它们应该具有相同的长度则抛出异常,等等。


7
投票

在 .NET 4 中,您可以在

IEnumerable<T>

上使用 .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
}

但是,它不会抛出不等的长度。不过,您始终可以测试一下。


3
投票

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

3
投票
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
    }
}

2
投票

使用

Zip
功能,如

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

不过,这不会引发异常......


1
投票

你可以做这样的事情。

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 可以按照你想要的方式执行(据我所知)。


1
投票

自从 C# 7.0 引入元组以来,您可以创建一个返回

Lockstep
的通用
IEnumerable<(T1, T2)>
函数:

public static IEnumerable<(T1, T2)> Lockstep<T1, T2>(IEnumerable<T1> t1s, IEnumerable<T2> t2s)
{
    using IEnumerator<T1> enum1 = t1s.GetEnumerator();
    using IEnumerator<T2> enum2 = t2s.GetEnumerator();
    while (enum1.MoveNext() && enum2.MoveNext())
        yield return (enum1.Current, enum2.Current);
}

并像这样使用它:

void LockstepDemo(IEnumerable<A> xs, IEnumerable<B> ys)
{
    foreach (var (x, y) in Lockstep(xs, ys))
        Consume(x, y);
}

如果需要三个或更多枚举,可以使用三个或更多参数重载

Lockstep

© www.soinside.com 2019 - 2024. All rights reserved.