Linq中的Enumerable.Zip扩展方法有什么用途?

170

Enumerable.Zip是Linq中的扩展方法,用于将两个序列合并成一个序列。


2
你是指这个吗:http://msdn.microsoft.com/en-us/library/dd267698.aspx?- 你想要实现什么目标? - anon271334
21
就像拉链的两边合拢一样。 - Kevin Panko
可能是什么是zip函数的目的(如Python或C# 4.0中)?的重复问题。 - Giulio Caccin
9个回答

245

Zip操作符使用指定的选择器函数合并两个序列相应的元素。

var letters= new string[] { "A", "B", "C", "D", "E" };
var numbers= new int[] { 1, 2, 3 };
var q = letters.Zip(numbers, (l, n) => l + n.ToString());
foreach (var s in q)
    Console.WriteLine(s);

输出

A1
B2
C3

59
我喜欢这个答案,因为它展示了当元素数量不匹配时会发生什么,类似于msdn文档 - DLeh
2
如果我希望zip在一个列表用尽元素后继续,该怎么办?在这种情况下,较短的列表元素应采用默认值。此时的输出应为A1、B2、C3、D0、E0。 - liang
2
@liang 两个选择:A)编写自己的Zip替代方案。B)编写一个方法来yield return短列表的每个元素,然后继续无限期地yield return default。 (选项B要求您事先知道哪个列表较短。) - jpaugh
你也可以尝试使用.SelectMany()方法来处理两个列表长度不同的情况:https://dev59.com/CnNA5IYBdhLWcg3wcddk#13035259 - n0099
你还可以尝试使用.SelectMany()来处理两个列表的不同长度:https://stackoverflow.com/questions/958949/difference-between-select-and-selectmany/13035259#13035259 - undefined

122

Zip 用于将两个序列合并为一个。例如,如果您有以下序列:

1, 2, 3

并且

10, 20, 30

你希望得到一个序列,这个序列是将每个序列中相同位置的元素相乘得到的结果。

10, 40, 90

你可以这样说

var left = new[] { 1, 2, 3 };
var right = new[] { 10, 20, 30 };
var products = left.Zip(right, (m, n) => m * n);

zip操作被称为"zip",因为你可以将其中一个序列看作拉链的左侧,另一个序列视为右侧,而zip操作会将两侧元素(序列的元素)合并在一起。


9
这绝对是这里最好的解释。 - Maxim Gershkovich
3
喜欢拉链的例子,非常自然。我最初的印象是它是否与速度有关,就像你在车上穿过一条街道一样快。 - RBT
解释为什么它被称为“Zip”很好,现在我更有可能记住它。 - marcos.borunda

25

它遍历两个序列并将它们的元素逐个组合成一个新的序列。因此,您取出序列A中的一个元素,使用来自序列B的相应元素进行转换,结果形成序列C的一个元素。

一种思考方法是它类似于Select,只不过不是对单个集合中的项进行转换,而是同时作用于两个集合。

MSDN关于该方法的文章中可以了解到:

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

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

foreach (var item in numbersAndWords)
    Console.WriteLine(item);

// This code produces the following output:

// 1 one
// 2 two
// 3 three

如果你想使用命令式代码实现这个功能,你可能会像这样做:

for (int i = 0; i < numbers.Length && i < words.Length; i++)
{
    numbersAndWords.Add(numbers[i] + " " + words[i]);
}

如果 LINQ 中没有 Zip,你可以这样做:

var numbersAndWords = numbers.Select(
                          (num, i) => num + " " + words[i]
                      );

当您的数据分散在类似数组的简单列表中时,并且每个列表具有相同的长度和顺序,并且每个列表描述了同一组对象的不同属性时,这将非常有用。 Zip 帮助您将这些数据片段组合成更连贯的结构。

因此,如果您有一个州名数组和另一个对应的州名缩写数组,您可以将它们整合到一个 State 类中,如下所示:

IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
    return stateNames.Zip(statePopulations, 
                          (name, population) => new State()
                          {
                              Name = name,
                              Population = population
                          });
}

我也喜欢这个答案,因为它提到了与“Select”相似之处。 - John Smith

21

不要被名称Zip所迷惑,它与文件或文件夹的压缩无关。实际上,它的名称来自于衣服上拉链的工作方式:衣服上的拉链有两侧,每侧都有许多牙齿。当您向一个方向移动时,拉链会枚举(移动)两侧并通过咬合牙齿关闭拉链。当您向另一个方向移动时,它会打开牙齿。您最终会得到一个开放或关闭的拉链。

Zip方法的概念与此相同。考虑一个例子,我们有两个集合。一个存储字母,另一个存储以该字母开头的食品名称。为了清晰起见,我称它们为leftSideOfZipperrightSideOfZipper。以下是代码:

var leftSideOfZipper = new List<string> { "A", "B", "C", "D", "E" };
var rightSideOfZipper = new List<string> { "Apple", "Banana", "Coconut", "Donut" };

我们的任务是制作一个集合,其中水果的字母由冒号:分隔,并附上它们的名称。就像这样:
A : Apple
B : Banana
C : Coconut
D : Donut
< p> Zip 及时出手。为了跟上我们的拉链术语,我们将把这个结果称为< code> closedZipper ,左侧拉链的项目称为< code> leftTooth ,右侧称为< code> righTooth 显而易见的原因:
var closedZipper = leftSideOfZipper
   .Zip(rightSideOfZipper, (leftTooth, rightTooth) => leftTooth + " : " + rightTooth).ToList();

在上面的代码中,我们正在枚举(遍历)拉链的左侧和右侧,并对每个“齿”执行操作。我们执行的操作是将左齿(食品编号)与:和右齿(食品名称)连接起来。 我们使用以下代码实现:

(leftTooth, rightTooth) => leftTooth + " : " + rightTooth)

最终结果是这样的:

A : Apple
B : Banana
C : Coconut
D : Donut

最后一个字母 E 哪去了?

如果你正在拉一条真正的衣服拉链,其中一侧(无论是左侧还是右侧)的齿数比另一侧少,会发生什么? 好吧,拉链就会停在那里。 Zip 方法也会做同样的事情:一旦达到任一侧的最后一项,就会停止。 在我们的例子中,右侧的齿数(食品名称)较少,因此它将停止在 "甜甜圈"。


1
是的,一开始名为“Zip”的名称可能会让人感到困惑。也许“交错”或“编织”会是更具描述性的方法名称。 - Lance U. Matthews
3
@bacon 是的,但那样我就不能用拉链的例子了 ;) 我认为一旦你明白它像拉链一样,之后就很简单明了了。 - CodingYoshi

16
很多答案都展示了 Zip,但是并没有真正解释使用 Zip 的现实用例。Zip 特别适合迭代连续的一对内容。这可以通过对集合 X 自身进行迭代并跳过一个元素来完成:x.Zip(x.Skip(1))。可视化示例如下:
 x | x.Skip(1) | x.Zip(x.Skip(1), ...)
---+-----------+----------------------
   |    1      |
 1 |    2      | (1, 2)
 2 |    3      | (2, 1)
 3 |    4      | (3, 2)
 4 |    5      | (4, 3)

这些连续的配对对于查找值之间的第一个差异很有用。例如,IEnumable<MouseXPosition> 的连续配对可以用来生成 IEnumerable<MouseXDelta>。类似地,按钮的采样布尔值可以被解释为事件,如NotPressed/Clicked/Held/Released。这些事件可以驱动调用委托方法。下面是一个示例:

using System;
using System.Collections.Generic;
using System.Linq;

enum MouseEvent { NotPressed, Clicked, Held, Released }

public class Program {
    public static void Main() {
        // Example: Sampling the boolean state of a mouse button
        List<bool> mouseStates = new List<bool> { false, false, false, false, true, true, true, false, true, false, false, true };

        mouseStates.Zip(mouseStates.Skip(1), (oldMouseState, newMouseState) => {
            if (oldMouseState) {
                if (newMouseState) return MouseEvent.Held;
                else return MouseEvent.Released;
            } else {
                if (newMouseState) return MouseEvent.Clicked;
                else return MouseEvent.NotPressed;
            }
        })
        .ToList()
        .ForEach(mouseEvent => Console.WriteLine(mouseEvent) );
    }
}

打印:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked

8

我没有足够的声望在评论区发帖,但是回答相关问题:

如果我想让zip继续处理其中一个列表到达元素结尾后怎么办?这种情况下,较短的列表元素应该使用默认值。此时输出为A1、B2、C3、D0、E0。-2015年11月19日liang 3:29

您需要使用Array.Resize()将较短的序列填充到默认值,然后将它们Zip()在一起。

代码示例:

var letters = new string[] { "A", "B", "C", "D", "E" };
var numbers = new int[] { 1, 2, 3 };
if (numbers.Length < letters.Length)
    Array.Resize(ref numbers, letters.Length);
var q = letters.Zip(numbers, (l, n) => l + n.ToString());
foreach (var s in q)
    Console.WriteLine(s);

输出:

A1
B2
C3
D0
E0

请注意,使用Array.Resize()有一个注意事项:C#中的Redim Preserve? 如果不知道哪个序列更短,可以创建一个函数来解决这个问题:
static void Main(string[] args)
{
    var letters = new string[] { "A", "B", "C", "D", "E" };
    var numbers = new int[] { 1, 2, 3 };
    var q = letters.Zip(numbers, (l, n) => l + n.ToString()).ToArray();
    var qDef = ZipDefault(letters, numbers);
    Array.Resize(ref q, qDef.Count());
    // Note: using a second .Zip() to show the results side-by-side
    foreach (var s in q.Zip(qDef, (a, b) => string.Format("{0, 2} {1, 2}", a, b)))
        Console.WriteLine(s);
}

static IEnumerable<string> ZipDefault(string[] letters, int[] numbers)
{
    switch (letters.Length.CompareTo(numbers.Length))
    {
        case -1: Array.Resize(ref letters, numbers.Length); break;
        case 0: goto default;
        case 1: Array.Resize(ref numbers, letters.Length); break;
        default: break;
    }
    return letters.Zip(numbers, (l, n) => l + n.ToString()); 
}

普通的.Zip()和ZipDefault()的输出结果:

A1 A1
B2 B2
C3 C3
   D0
   E0

回到原问题的主要答案,当需要"压缩"的序列长度不同时,另一个有趣的事情是希望以连接它们的方式使列表的结尾匹配而不是顶部。这可以通过使用.Skip()来“跳过”适当数量的项目来实现。

foreach (var s in letters.Skip(letters.Length - numbers.Length).Zip(numbers, (l, n) => l + n.ToString()).ToArray())
Console.WriteLine(s);

输出:

C1
D2
E3

调整大小是浪费的,特别是如果其中一个集合很大。你真正想做的是有一个枚举,在集合结束后继续,按需用空值填充它(没有支持集合)。你可以使用以下代码实现:public static IEnumerable Pad(this IEnumerable input, long minLength, T value = default(T)) { long numYielded = 0; foreach (T element in input) { yield return element; ++numYielded; } while (numYielded < minLength) { yield return value; ++numYielded; } } - Pagefault
似乎我不确定如何在注释中成功格式化代码... - Pagefault

7

正如其他人所述,Zip 可以让您将两个集合组合在一起,以便在进一步的 Linq 语句或 foreach 循环中使用。

以前需要使用 for 循环和两个数组才能完成的操作现在可以使用匿名对象在 foreach 循环中完成。

我刚刚发现的一个示例有点傻,但如果并行化是有益的,那么可能会很有用,它是一个带副作用的单行队列遍历:

timeSegments
    .Zip(timeSegments.Skip(1), (Current, Next) => new {Current, Next})
    .Where(zip => zip.Current.EndTime > zip.Next.StartTime)
    .AsParallel()
    .ForAll(zip => zip.Current.EndTime = zip.Next.StartTime);

timeSegments代表队列中当前或已出队的项(最后一个元素被Zip截断)。 timeSegments.Skip(1)代表队列中下一个或预取的项。 Zip方法将这两个对象合并为一个匿名对象,具有Next和Current属性。 然后我们使用Where进行过滤,并使用AsParallel().ForAll进行更改。 当然,最后一部分可以是常规foreach或另一个返回有问题时间段的Select语句。


1
太棒了,正是我想要的!在zip后面可以使用where和其他命令。 - Chandraprakash

3
Zip方法允许您使用由调用者提供的合并函数“合并”两个不相关的序列。 MSDN上的示例实际上非常好地演示了Zip的用法。 在此示例中,您可以使用任意函数(在本例中仅将来自两个序列的项连接为单个字符串)来组合任意不相关的序列。请保留HTML标记。
int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

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

foreach (var item in numbersAndWords)
    Console.WriteLine(item);

// This code produces the following output:

// 1 one
// 2 two
// 3 three

0
string[] fname = { "mark", "john", "joseph" };
string[] lname = { "castro", "cruz", "lopez" };

var fullName = fname.Zip(lname, (f, l) => f + " " + l);

foreach (var item in fullName)
{
    Console.WriteLine(item);
}
// The output are

//mark castro..etc

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