简体   繁体   English

如何同时遍历两个 IEnumerables?

[英]How to iterate through two IEnumerables simultaneously?

I have two enumerables: IEnumerable<A> list1 and IEnumerable<B> list2 .我有两个枚举: IEnumerable<A> list1IEnumerable<B> list2 I would like to iterate through them simultaneously like:我想同时遍历它们,例如:

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

If they don't contain the same number of elements, an exception should be thrown.如果它们不包含相同数量的元素,则应抛出异常。

What is the best way to do this?做这个的最好方式是什么?

You want something like the Zip LINQ operator - but the version in .NET 4 always just truncates when either sequence finishes. 你需要类似Zip LINQ运算符的东西 - 但是.NET 4中的版本总是在任一序列完成时截断。

The MoreLINQ implementation has an EquiZip method which will throw an InvalidOperationException instead. MoreLINQ实现有一个EquiZip方法,它将抛出一个InvalidOperationException

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

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

Here's an implementation of this operation, typically called Zip: 这是此操作的一个实现,通常称为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);
            }
        }
    }
}

To make it throw an exception if just one of the sequences run out of values, change the while-loop so: 如果只有其中一个序列用完了值,则使其抛出异常,更改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++;
}

In short, the language offers no clean way to do this. 简而言之,该语言没有提供干净的方法来做到这一点。 Enumeration was designed to be done over one enumerable at a time. 枚举的目的是一次完成一个可枚举。 You can mimic what foreach does for you pretty easily: 你可以很容易地模仿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
    }

What to do if they are of different length is up to you. 如果它们长度不同,该怎么办取决于你。 Perhaps find out which one still has elements after the while loop is done and keep working with that one, throw an exception if they should be the same length, etc. 也许在while循环完成后找出哪个仍然有元素并继续使用那个元素,如果它们应该是相同的长度,则抛出异常等。

In .NET 4, you can use the .Zip extension method on IEnumerable<T> 在.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
}

It won't throw on unequal lengths, however. 然而,它不会抛出不等长度。 You can always test that, though. 不过,你可以随时测试。

Go with IEnumerable.GetEnumerator, so you can move around the enumerable. 使用IEnumerable.GetEnumerator,这样你就可以移动枚举。 Note that this might have some really nasty behavior, and you must be careful. 请注意,这可能有一些非常讨厌的行为,你必须小心。 If you want to get it working, go with this, if you want to have maintainable code, use two foreach. 如果你想让它工作,那么,如果你想拥有可维护的代码,请使用两个foreach。

You could create a wrapping class or use a library (as Jon Skeet suggests) to handle this functionality in a more generic way if you are going to use it more than once thru your code. 您可以创建一个包装类或使用库(如Jon Skeet所建议的)以更通用的方式处理此功能,如果您要通过代码多次使用它。

The code for what I suggest: 我建议的代码:

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

You can do something like this. 你可以做这样的事情。

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# has no foreach that can do it how you want (that I am aware of). C#没有foreach可以按你想要的那样做(我知道)。

Use the Zip function like 使用Zip函数就好

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

This doesn't throw an exception, though ... 这不会引发异常,但......

Since C# 7.0 introduced tuples, you can create a generic Lockstep function that returns an IEnumerable<(T1, T2)> :由于 C# 7.0 引入了元组,您可以创建一个返回IEnumerable<(T1, T2)>的通用Lockstep function:

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

And use it like this:并像这样使用它:

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

This can be easily extended to allow for three or more enumerations.这可以很容易地扩展以允许三个或更多枚举。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM