同时遍历多个IEnumerable<T>

3
有几个 IEnumerable<T> 的实例,我想先获取每个实例的第一个元素,然后是第二个元素,依此类推。
如果它们是列表,我会这样做:
List<List<T>> myLists = ...;
bool iLessThanMaxSize = true;
for (int i=0; iLessThanMaxSize; i++)
{
    iLessThanMaxSize = false;
    for (int j=0; j<myLists.Count; j++)
    {
        if (i < myLists[j].Count)
        {
            iLessThanMaxSize = true;
            yield return myLists[j][i];
        }
    }
}

然而,它们并不是列表,而是IEnumerables,这是很重要的,因为项目是在一个繁重的计算中按需计算的,所以一次计算所有项目所需的时间比仅按需获取有限数量的项目更长。
我如何在使用IEnumerable而不是List的情况下获得相同的行为?

编辑:我所说的“同时”并不是指多线程,而只是枚举的模式,即先获取每个集合的第一个元素,然后获取每个集合的第二个元素,而不是先枚举第一个集合,然后再枚举第二个集合,依此类推(这将是将IEnumerables连接起来)。对于造成的混淆,我表示抱歉。


编辑:在我的特定用例中,对集合的枚举将继续,直到枚举所有集合的所有元素为止。如果元素计数不同,已经完全枚举的集合将被跳过。

1
同时指的是在多个线程上并行进行吗? - undefined
如果列表一的第一个元素要比列表二的第二个元素花费更长的时间才能获得,会发生什么情况呢?所以第一个列表的“加载”时间比第二个列表长?所以有可能需要访问列表一的第一个元素,但它还没有出现,而列表二已经有了这样一个元素。所以元素的“位置”重要还是它们的“数量”重要? - undefined
你正在处理的是 List<IEnumerable<T>> 还是 IEnumerable<IEnumerable<T>> - undefined
这里有一些我认为适合你问题的其他解决方案:https://stackoverflow.com/questions/2427015/how-to-do-pythons-zip-in-c - undefined
@ZoharPeled 这是一个 List<IEnumerable<T>> - undefined
4个回答

4
如果你想对IEnumerables进行更复杂的操作,通常需要获取枚举器并使用.MoveNext()和.Current来完成操作。
例如:
public static IEnumerable<T> MyFunction<T>(IEnumerable<IEnumerable<T>> enumerables)
{
    try{
       var enumerators = enumerables.Select(e => e.GetEnumerator()).ToList();
       bool anyResult ;
      do
       {
           anyResult = false;
           foreach (var enumerator in enumerators)
           {
               if (enumerator.MoveNext())
               {
                   yield return enumerator.Current;
                   anyResult = true;
               }
           }
       } while (anyResult);
    }
    finally{
       foreach (var enumerator in enumerators)
       {
           enumerator.Dispose();
       }
    }
}

[Test]
public void Test()
{
    var l1 = new[] { 1, 4 };
    var l2 = new[] { 2, 5, 6 };
    var l3 = new[] { 3, };
    var result = MyFunction(new[] { l1, l2, l3 });
    CollectionAssert.AreEqual(new []{1, 2, 3, 4, 5, 6}, result);
}

我建议在处理大量数据时避免使用这样的代码。当数据量增长时,通常希望使用更低级别、更少抽象的代码来确保足够的性能。

1
请在finally块中实现foreach (var enumerator in enumerators) { enumerator.Dispose(); };否则,如果我们在MyFunction<T>完成其主循环之前离开它,例如Iterate(list).Take(5)可以枚举前5个项目,然后离开MyFunctions而不进行处理,我们可能会面临资源泄漏的问题。 - undefined

2
如果你想要高性能并且想要手动枚举,你可以这样实现:
private static IEnumerable<T> MyFunc<T>(IEnumerable<IEnumerable<T>> source) {
  Queue<IEnumerator<T>> agenda = new(source.Select(inner => inner.GetEnumerator()));

  try {
    while (agenda.Count > 0) {
      var en = agenda.Peek();

      if (en.MoveNext()) {
        agenda.Enqueue(agenda.Dequeue());

        yield return en.Current;
      }
      else
        agenda.Dequeue().Dispose();
    }
  }
  finally {
    foreach (var en in agenda)
      en.Dispose();
  }
}

演示:
List<List<int>> list = new () {
  new() { 1, 2, 3 },
  new() { 4 },
  new() { 5, 6 },
};

var report = string.Join(", ", MyFunc(list));

Console.Write(report);

输出:

1, 4, 5, 2, 6, 3

1
@Theodor Zoulias:即使看起来是一个学术问题,但这是一个有趣的问题,谢谢!我已编辑答案,以处理en.MoveNext()抛出异常的情况。 - undefined

1
你可以使用枚举器(参见IEnumerator<T>接口)来遍历集合中的集合:
IEnumerable<T> Iterate<T>(IEnumerable<IEnumerable<T>> cols)
{
    bool hasNext;
    List<IEnumerator<T>> enumerators = null;
    try
    {
        enumerators = cols.Select(c => c.GetEnumerator()).ToList();
        do
        {
            hasNext = false;
            foreach (var enumerator in enumerators)
            {
                if (enumerator.MoveNext())
                {
                    yield return enumerator.Current;
                    hasNext = true;
                }
            }
        } while (hasNext);
    }
    finally
    {
        if (enumerators is not null)
        {
            foreach (var e in enumerators)
            {
                e.Dispose();
            }
        }
    }
}

1
请在finally块中实现foreach (var e in enumerators) { e.Dispose(); };否则,如果我们在Iterate<T>完成其主循环之前离开它,例如Iterate(list).Take(5)可以枚举前5个项目,然后离开Iterate而不进行处理,我们可能会面临资源泄漏的问题。 - undefined
@DmitryBychenko 谢谢,已修复。原本是有的,但出于某种原因我决定删除它。 - undefined

1
这里是另一种实现,与JonasH、Dmitry Bychenko和Guru Stron发布的实现在功能上完全相同。我的贡献主要是Merge是该方法的更好名称,因为其他类型序列的类似API通常都是这样命名的(类似的API)。另外,params IEnumerable<T>[]是更好的签名,因为接受IEnumerable<IEnumerable<T>>会对外部序列的延迟执行产生错误的期望。
/// <summary>
/// Merges all elements from all source sequences, into a single interleaved sequence.
/// </summary>
public static IEnumerable<TSource> Merge<TSource>(
    params IEnumerable<TSource>[] sources)
{
    ArgumentNullException.ThrowIfNull(sources);
    List<IEnumerator<TSource>> enumerators = new(sources.Length);
    try
    {
        foreach (IEnumerable<TSource> source in sources)
            enumerators.Add(source.GetEnumerator());
        while (enumerators.Count > 0)
        {
            for (int i = 0; i < enumerators.Count; i++)
            {
                IEnumerator<TSource> enumerator = enumerators[i];
                if (enumerator.MoveNext())
                {
                    yield return enumerator.Current;
                }
                else
                {
                    enumerators.RemoveAt(i);
                    enumerator.Dispose();
                    i--;
                }
            }
        }
    }
    finally
    {
        foreach (IEnumerator<TSource> e in enumerators)
            e.Dispose();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接