将Zip和IEnumerable<T>结合在一起?同时迭代它们?

8

我有:

IEnumerable<IEnumerable<T>> items;

and I'd like to create:-

IEnumerable<IEnumerable<T>> results;

"results"中的第一项是“items”中每个IEnumerable的第一项的IEnumerable,"results"中的第二项是"items"中每个IEnumerable的第二项的IEnumerable,以此类推。

这些IEnumerables的长度不一定相同。如果在items中的某些IEnumerables在特定索引处没有元素,则我期望在results中匹配的IEnumerable将具有较少的项。

例如:

items = { "1", "2", "3", "4" } , { "a", "b", "c" };
results = { "1", "a" } , { "2", "b" }, { "3", "c" }, { "4" };

编辑:另一个示例(评论中请求):

items = { "1", "2", "3", "4" } , { "a", "b", "c" }, { "p", "q", "r", "s", "t" };
results = { "1", "a", "p" } , { "2", "b", "q" }, { "3", "c", "r" }, { "4", "s" }, { "t" };

我事先不知道有多少个序列,也不知道每个序列中有多少个元素。我可能有1000个包含100万个元素的序列,但我只需要前10个,所以如果可以的话,我想使用源序列的(惰性)枚举。特别是如果可以避免创建新的数据结构。
是否有内置方法(类似于IEnumerable.Zip)可以做到这一点?
还有其他方法吗?

如果items包含三个序列,会发生什么? - Richard Szalay
这类似于“如何同时迭代两个数组?”(涵盖了N = 2的情况)的问题。 - James McNellis
请查看Eric Lippert的博客(该博客是从一个SO问题的答案扩展而来),关于在任意多个序列上计算笛卡尔积。http://blogs.msdn.com/b/ericlippert/archive/2010/06/28/computing-a-cartesian-product-with-linq.aspx - Anthony Pegram
除了对不同长度的要求外,这听起来很像矩阵的转置。我在这里为LINQ编写了一个Transpose方法:https://dev59.com/_XI95IYBdhLWcg3w-DDz#2070434 - dtb
@Richard:添加了一个例子。 @James:是的,但如果您事先知道有多少个序列,那就很容易了 :( @Anthony:唉,Eric可以使用累加器,因此不需要枢轴。 @dtb:您的转置方法看起来是一个很好的开始。 - Iain Galloway
你已经有了好的答案,而且我现在没有电脑来测试,但是你应该能够在运行时调用 MakeGenericMethod 并获取所需的 Zip 方法。 - Mark Hurd
6个回答

7
现在进行了轻微测试,并且处置工作正常。
public static class Extensions
{
  public static IEnumerable<IEnumerable<T>> JaggedPivot<T>(
    this IEnumerable<IEnumerable<T>> source)
  {
    List<IEnumerator<T>> originalEnumerators = source
      .Select(x => x.GetEnumerator())
      .ToList();

    try
    {
      List<IEnumerator<T>> enumerators = originalEnumerators
        .Where(x => x.MoveNext()).ToList();

      while (enumerators.Any())
      {
        List<T> result = enumerators.Select(x => x.Current).ToList();
        yield return result;
        enumerators = enumerators.Where(x => x.MoveNext()).ToList();
      }
    }
    finally
    {
      originalEnumerators.ForEach(x => x.Dispose());
    }
  } 
}

public class TestExtensions
{
  public void Test1()
  {
    IEnumerable<IEnumerable<int>> myInts = new List<IEnumerable<int>>()
    {
      Enumerable.Range(1, 20).ToList(),
      Enumerable.Range(21, 5).ToList(),
      Enumerable.Range(26, 15).ToList()
    };

    foreach(IEnumerable<int> x in myInts.JaggedPivot().Take(10))
    {
      foreach(int i in x)
      {
        Console.Write("{0} ", i);
      }
      Console.WriteLine();
    }
  }
}

3
请注意,您没有处理任何迭代器,这可能会根据它们正在迭代的内容而产生问题。 - Jon Skeet
有趣。我不知道在这些迭代器方法中,当存在惰性时,finally语句会被激活。不过这是有道理的,因为调用方最终会对惰性的IEnumerator对象调用Dispose方法。 - Amy B
我已经接受了这个答案,但请确保您也阅读了Jon Skeet的答案。 - Iain Galloway

4

如果你可以保证结果的使用顺序,那么这个过程相对简单。但是,如果结果可能以任意顺序使用,你可能需要缓存所有数据。考虑以下情况:

如果您能够保证结果使用的顺序,那么这个过程就相对简单了。但是,如果结果可能以任意顺序使用,您可能需要缓存所有内容。请考虑以下示例:

var results = MethodToBeImplemented(sequences);
var iterator = results.GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
iterator.MoveNext();
var second = iterator.Current;
foreach (var x in second)
{
    // Do something
}
foreach (var x in first)
{
    // Do something
}

为了获取“second”中的项目,您需要遍历所有子序列,跳过第一个项目。如果您想要遍历“first”中的项目,则需要记住这些项目或准备重新评估子序列,才能使其有效。
同样地,您需要将子序列作为IEnumerable值缓冲区,或每次重新读取整个内容。
基本上,这是一个难以优雅地完成的问题,无法令所有情况都愉快地工作:( 如果您有一个具有适当限制的特定情况,我们可能可以提供更多帮助。

谢谢!我可以保证结果将按顺序使用。这是否减轻了DavidB答案中“ToList”调用的需要?(现在进行测试) - Iain Galloway
@Iain:我认为是这样,尽管正确实现可能相当复杂。您仍需要将子序列引用本身作为“IEnumerable<T>”进行缓冲。嗯。而不是返回“IEnumerable<IEnumerable<T>>”,您可以传递要在每个值上执行的操作之类的内容吗?这将使事情变得简单得多! - Jon Skeet
我认为我不能传递一个Action(虽然这是个好主意,但我会记住它以备将来遇到类似的问题!)。在这个阶段,我认为如果让它变得优雅会使它变得超级复杂,那么我就直接内联所有内容。 - Iain Galloway

1

根据David B的回答,这段代码应该性能更好:

public static IEnumerable<IEnumerable<T>> JaggedPivot<T>(
    this IEnumerable<IEnumerable<T>> source)
{
    var originalEnumerators = source.Select(x => x.GetEnumerator()).ToList();
    try
    {
        var enumerators =
            new List<IEnumerator<T>>(originalEnumerators.Where(x => x.MoveNext()));

        while (enumerators.Any())
        {
            yield return enumerators.Select(x => x.Current).ToList();
            enumerators.RemoveAll(x => !x.MoveNext());
        }
    }
    finally
    {
        originalEnumerators.ForEach(x => x.Dispose());
    }
}

区别在于枚举变量不会每次都被重新创建。

0

这里有一个比较短,但毫无疑问不太高效的例子:

Enumerable.Range(0,items.Select(x => x.Count()).Max())
    .Select(x => items.SelectMany(y => y.Skip(x).Take(1)));

与AS-CII的答案类似,这会对源代码频繁调用GetEnumerator()方法。 - Iain Galloway

0

这个怎么样?

        List<string[]> items = new List<string[]>()
        {
            new string[] { "a", "b", "c" },
            new string[] { "1", "2", "3" },
            new string[] { "x", "y" },
            new string[] { "y", "z", "w" }
        };

        var x = from i in Enumerable.Range(0, items.Max(a => a.Length))
                select from z in items
                       where z.Length > i
                       select z[i];

如果我已经将数据以可随机访问的形式存储在内存中,那么这将非常容易。不幸的是,我需要对源 IEnumerables 进行惰性枚举。调用 z[i] - 或者 z.ElementAt(i) - 会破坏这一点。 - Iain Galloway

0
你可以像这样组合已有的操作符:
IEnumerable<IEnumerable<int>> myInts = new List<IEnumerable<int>>()
    {
        Enumerable.Range(1, 20).ToList(),
        Enumerable.Range(21, 5).ToList(),
        Enumerable.Range(26, 15).ToList()
    };

myInts.SelectMany(item => item.Select((number, index) => Tuple.Create(index, number)))
      .GroupBy(item => item.Item1)
      .Select(group => group.Select(tuple => tuple.Item2));

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