如何合并(或压缩)两个IEnumerables?

9

我有一个 IEnumerable<T> 和一个 IEnumerable<U>,我想将它们合并成一个 IEnumerable<KeyValuePair<T,U>>,其中合并在 KeyValuePair 中的元素索引相同。请注意,我不使用 IList,因此我没有要合并的项目数量或索引。如何最好地完成这个任务?我希望得到一个 LINQ 的答案,但任何能够优雅地完成工作的方法都可以。


Eric Lippert又发布了一篇博客文章:Zip Me Up - n8wrl
有趣的是 - 我昨晚刚读过这个。=) - Erik Forbes
2
从.NET 4.0开始,框架附带了一个IEnumerable Zip扩展方法。 - Joey Adams
10个回答

18

注意:自 .NET 4.0 版本起,框架在 IEnumerable 上包含了一个名为 .Zip 的扩展方法,这里有文档。以下内容仅供参考并适用于 .NET 4.0 之前的版本。

我使用了以下这些扩展方法:

// From http://community.bartdesmet.net/blogs/bart/archive/2008/11/03/c-4-0-feature-focus-part-3-intermezzo-linq-s-new-zip-operator.aspx
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> func) {
    if (first == null) 
        throw new ArgumentNullException("first");
    if (second == null) 
        throw new ArgumentNullException("second");
    if (func == null)
        throw new ArgumentNullException("func");
    using (var ie1 = first.GetEnumerator())
    using (var ie2 = second.GetEnumerator())
        while (ie1.MoveNext() && ie2.MoveNext())
            yield return func(ie1.Current, ie2.Current);
}

public static IEnumerable<KeyValuePair<T, R>> Zip<T, R>(this IEnumerable<T> first, IEnumerable<R> second) {
    return first.Zip(second, (f, s) => new KeyValuePair<T, R>(f, s));
}

编辑:在评论区的建议下,我有必要澄清和修正一些内容:

  • 我最初直接从Bart De Smet的博客中引用了第一个Zip实现。
  • 添加了枚举器的释放(这也是Bart原始帖子中提到的)。
  • 添加了空参数检查(也在Bart的帖子中讨论过)。

这种方式会鼓励“调用者”做出假设。它只能做唯一的事情,有时候这种假设是有根据的。 - Joel Coehoorn
你也应该处理你的枚举。 - Reed Copsey
好的。不过,如果你不小心传递给它错误的类型,你会自己给自己惹麻烦。 - Welbog
@reed:看看Bart的原始文章(http://community.bartdesmet.net/blogs/bart/archive/2008/11/03/c-4-0-feature-focus-part-3-intermezzo-linq-s-new-zip-operator.aspx),它涵盖了处理和其他问题。 - Mauricio Scheffer

17

针对那些偶然经过这个问题的人,需要更新一下信息,.Net 4.0原生支持此功能,例如Microsoft提供的示例:

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);

文档:

此方法将第一个序列的每个元素与第二个序列中具有相同索引的元素合并。如果这些序列的元素数量不相等,则该方法会将它们合并,直到其中一个序列结束为止。例如,如果一个序列包含三个元素,而另一个序列包含四个元素,则结果序列将只包含三个元素。


1

我会使用类似于以下的东西 -

IEnumerable<KeyValuePair<T,U>> Merge<T,U>(IEnumerable<T> keyCollection, IEnumerable<U> valueCollection)
{
    var keys = keyCollection.GetEnumerator();
    var values = valueCollection.GetEnumerator();
    try
    { 
        keys.Reset();
        values.Reset();

        while (keys.MoveNext() && values.MoveNext())
        {
            yield return new KeyValuePair<T,U>(keys.Current,values.Current);
        }
    }
    finally
    {
        keys.Dispose();
        values.Dispose();
    }
}

这应该能正常工作,并且之后可以正确清理。


1
我认为称其为“zip”是一个好的形式,因为这是函数式世界中已知的操作。 - Daniel

1

仔细考虑一下你在这里的要求:

你想要将两个IEnumerables合并在一起,其中“KeyValuePair中元素的索引是相同的”,但是你“没有计数或者要合并的项的索引”。

不能保证你的IEnumerables是排序还是未排序的。你的两个IEnumerable对象之间没有关联,所以你如何期望它们能够相互关联呢?


@welbog:看起来有一个问题的误解。我认为Erik所说的“索引”是IEnumerable中元素的位置(第1个,第2个等等)。 - Mauricio Scheffer
@mausch:一个不保证位置的位置。根据实现方式,两个IEnumerables的顺序可能不是预期的顺序。 - Welbog
@welbog:使用这样的可枚举对象调用Zip有意义吗?要么没有意义,要么调用者必须知道这一点...我看不到其他选择。 - Mauricio Scheffer
@mausch:我的观点是,问题本身无法按照所述方式解决。在这两个IEnumerables中的对象之间建立连接所需的信息片段不存在。你的解决方案做出了一个假设,这个假设添加了额外的信息,但如果假设不成立,它就会失败。 - Welbog
“你怎么能期望它们相互关联呢?”这是个有道理的观点,但我假设“索引”只是从IEnumerable中产生的顺序。在我的情况下,这就是我需要关联的全部内容。 - Erik Forbes
顺便说一句 - 给你点赞,因为你的担忧非常相关(如果在我的情况下完全可以预料和可控)。 - Erik Forbes

1

看看nextension

目前已实现的方法

IEnumerable

  • ForEach 对IEnumerable的每个元素执行指定的操作。
  • Clump 将项目分组成相同大小的批次。
  • Scan 通过将委托应用于IEnumerable中的项目对生成列表。
  • AtLeast 检查IEnumerable中至少有一定数量的项目。
  • AtMost 检查IEnumerable中没有超过一定数量的项目。
  • Zip 通过将两个其他列表合并为一个列表来创建列表。
  • Cycle 通过重复另一个列表来创建列表。

0

未经测试,但应该可以工作:

IEnumerable<KeyValuePair<T, U>> Zip<T, U>(IEnumerable<T> t, IEnumerable<U> u) {
    IEnumerator<T> et = t.GetEnumerator();
    IEnumerator<U> eu = u.GetEnumerator();

    for (;;) {
        bool bt = et.MoveNext();
        bool bu = eu.MoveNext();
        if (bt != bu)
            throw new ArgumentException("Different number of elements in t and u");
        if (!bt)
            break;
        yield return new KeyValuePair<T, U>(et.Current, eu.Current);
    }
}

0

这是来自Alexey Romanov的functional-dotnet项目的另一种实现:

/// <summary>
/// Takes two sequences and returns a sequence of corresponding pairs. 
/// If one sequence is short, excess elements of the longer sequence are discarded.
/// </summary>
/// <typeparam name="T1">The type of the 1.</typeparam>
/// <typeparam name="T2">The type of the 2.</typeparam>
/// <param name="sequence1">The first sequence.</param>
/// <param name="sequence2">The second sequence.</param>
/// <returns></returns>
public static IEnumerable<Tuple<T1, T2>> Zip<T1, T2>(
    this IEnumerable<T1> sequence1, IEnumerable<T2> sequence2) {
    using (
        IEnumerator<T1> enumerator1 = sequence1.GetEnumerator())
    using (
        IEnumerator<T2> enumerator2 = sequence2.GetEnumerator()) {
        while (enumerator1.MoveNext() && enumerator2.MoveNext()) {
            yield return
                Pair.New(enumerator1.Current, enumerator2.Current);
        }
    }
    //
    //zip :: [a] -> [b] -> [(a,b)]
    //zip (a:as) (b:bs) = (a,b) : zip as bs
    //zip _      _      = []
}

Pair.New替换为新的KeyValuePair<T1, T2>(以及返回类型),你就可以开始了。


0

0

MSDN有以下自定义序列运算符示例。Welbog是正确的;如果底层数据没有索引,您无法保证操作会按照您的期望执行。


0

JaredPar拥有一个,里面有很多有用的东西,其中包括Zip,这将使你想要做的事情成为可能。


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