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


Answers:


191

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

41
我喜欢这个答案,因为它显示了当元素数量不匹配时会发生什么,类似于msdn文档
DLeh

2
如果我想让zip在元素列表用完的地方继续执行该怎么办?在这种情况下,较短的list元素应采用默认值。在这种情况下,输出为A1,B2,C3,D0,E0。
liang

2
@liang两种选择:A)编写自己的Zip选择。B)写方法yield return较短列表的每个元素,然后继续yield return荷兰国际集团default上无限期。(选项B要求您提前知道哪个列表较短。)
jpaugh

105

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运算符会将两侧配对,从而将牙齿配对(序列中的元素)。


8
绝对是这里最好的解释。
Maxim Gershkovich 2012年

2
喜欢拉链的例子。很自然 我最初的印象是它是否与速度有关或类似,就像您在车上穿过街道一样。
RBT

23

它遍历两个序列并将它们的元素一个接一个地组合成一个新的序列。因此,您采用序列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
iliketocode

17

不要让这个名字Zip丢掉你。与压缩无关,就像压缩文件或文件夹(压缩)一样。它实际上是从衣服上的拉链的工作原理得名的:衣服上的拉链有2面,每面都有一束牙齿。当您朝一个方向前进时,拉链会枚举(移动)两侧,并通过咬紧牙齿来闭合拉链。当您朝另一个方向行驶时,它会张开牙齿。您要么打开拉链,要么关闭拉链。

该方法是相同的想法Zip。考虑一个例子,我们有两个集合。一个保存字母,另一个保存以该字母开头的食品名称。为了清楚起见,我呼吁他们leftSideOfZipperrightSideOfZipper。这是代码。

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

我们的任务是生产一个集合,该集合的果实字母以a :及其名称分隔。像这样:

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

Zip进行营救。为了跟上我们的拉链术语,我们将其称为结果closedZipper,将调用左侧拉链的项,将调用leftTooth右侧的项的righTooth原因显而易见:

var closedZipper = leftSideOfZipper
   .Zip(rightSideOfZipper, (leftTooth, rightTooth) => leftTooth + " : " + rightTooth).ToList();

在上面,我们枚举(行进)拉链的左侧和拉链的右侧,并对每个牙齿进行操作。我们正在执行的操作是将左齿(食物字母)与a串联:,然后将右齿(食物名称)连接起来。我们使用以下代码进行操作:

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

最终结果是这样的:

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

最后一个字母E怎么了?

如果要枚举(拉出)真衣服的拉链,并且一侧(左侧或右侧)都没有另一侧的牙齿少,那会发生什么?好吧,拉链会停在那里。该Zip方法将完全相同:一旦到达任一侧的最后一个项目,它将停止。在我们的情况下,右侧的牙齿(食物名称)较少,因此它将在“甜甜圈”处停止。


1
+1。是的,起初名称“ Zip”可能会令人困惑。也许“ Interleave”或“ Weave”是该方法的更具描述性的名称。
培根

1
@bacon是的,但是那以后我将无法使用我的拉链示例;)我想,一旦您弄清楚它就像一个拉链,之后就很简单了。
CodingYoshi

尽管我确切知道Zip扩展方法的作用,但我一直很好奇为什么命名为Zip。在软件的一般术语中,zip始终意味着其他含义。打个比方:-)你一定已经读懂了创作者的思想。
Raghu Reddy Muttana

7

我没有在评论部分张贴代表点,但要回答相关问题:

如果我想让zip在元素列表用完的地方继续执行该怎么办?在这种情况下,较短的list元素应采用默认值。在这种情况下,输出为A1,B2,C3,D0,E0。– liang 15年11月19日在3:29

您要做的是使用Array.Resize()将较短的序列填充为默认值,然后将它们压缩在一起。

代码示例:

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<T> Pad<T>(this IEnumerable<T> 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,但并没有真正解释会激发使用的现实用例Zip

一种特别常见的模式,Zip非常适合迭代连续的事物对。这是通过X自身迭代一个可枚举来完成的,跳过了1个元素: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>。类似地,采样bool的值button可以被interpretted到的事件,如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

6

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

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

我刚刚发现的一个例子很愚蠢,但是如果并行化是有益的,那么它可能会很有用,例如,单行队列遍历会产生副作用:

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语句。


3

Zip方法使您可以使用调用者自己提供的合并函数提供程序来“合并”两个不相关的序列。实际上,MSDN上的示例很好地展示了您可以使用Zip进行的操作。在此示例中,您采用了两个任意的,不相关的序列,并使用任意函数将它们组合在一起(在这种情况下,只需将两个序列中的项目串联到一个字符串中)。

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.