在 Linq 中使用 Enumery.Zip 扩展方法有什么用?

在 Linq 中使用 Enumerable.Zip扩展方法有什么用?

60873 次浏览

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

它迭代两个序列并将它们的元素一个接一个地组合成一个新的序列。所以你取出序列 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
});
}

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

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 操作员会适当地将两边拉到一起,将牙齿(序列的元素)分开。

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

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

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

我刚刚发现的一个例子有点傻,但是如果并行化是有益的,那么可以使用带有副作用的单行 Queue 遍历:

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 截断)。 Skip (1)表示队列中的下一个或查看项。 Zip 方法将这两个对象组合成一个具有 Next 和 Current 属性的匿名对象。 然后我们使用 Where 进行筛选,并使用 AsParall.ForAll 进行更改。 当然,最后一个位可以只是一个常规 foreach 或另一个 Select 语句,它返回有问题的时间段。

不要让 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

Zip前来救援。为了跟上我们的拉链术语,我们将这个结果称为 closedZipper,左边的拉链称为 leftTooth,右边的称为 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方法将完全做同样的事情: 它将在到达任何一边的最后一个项目后停止。在我们的情况下,右边有较少的牙齿(食品名称) ,所以它会停在“甜甜圈”。

我没有代表点可以发表在评论区,但是我可以回答相关的问题:

如果我希望在一个列表元素用完的地方继续执行 zip,该怎么办 在这种情况下,较短的列表元素应该采用默认值 在这种情况下是 A1,B2,C3,D0,E0。-梁19’15 at 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 # ?

如果不知道哪个序列是最短的,就可以创建一个函数来猜测它:

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

回到最初的问题 的主要答案,另一个有趣的事情是(当要“ zip”的序列的长度不同时)以这样一种方式将它们连接起来,这样列表的 结束就会匹配而不是顶部。这可以通过“跳过”使用。斯基普()。

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

产出:

C1
D2
E3

这里的许多答案都演示了 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>。类似地,取样的 buttonbool值可以解释为类似于 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