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

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

Zip运算符使用指定的select器函数合并两个序列的相应元素。

 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 

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); 

它被称为“拉链”,因为你把一个序列看作一个拉链的左侧,另一个序列作为拉链的右侧,拉链操作员将把双方拉在一起把牙齿(序列的元素)。

它遍历两个序列,并将它们的元素逐个合并成一个新的序列。 所以你取一个序列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允许您将两个集合组合起来用于进一步的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)表示队列中的下一个或每个peek项目。 Zip方法将这两者结合成一个具有Next和Current属性的匿名对象。 然后我们使用Where进行过滤,并使用AsParallel()。ForAll进行更改。 当然,最后一点可能只是一个普通的foreach或另一个Select语句,它返回违规的时间段。

不要让Zip这个名字把你扔掉。 它与压缩无关,如压缩文件或文件夹(压缩)。 它实际上从衣服上的拉链如何起作用:衣服上的拉链有两面,每面有一串牙齿。 朝一个方向行进时,拉链枚举(行进)两侧,并通过咬紧牙齿closures拉链。 当你走向另一个方向时,它会打开牙齿。 你要么拉链打开或closures结束。

这与Zip方法是一样的想法。 考虑一个例子,我们有两个集合。 一个人拿着信,另一个拿着一个以该信开头的食物的名字。 出于清晰的目的,我打电话给他们leftSideOfZipperrightSideOfZipper 。 这是代码。

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

我们的任务是制作一个collections品,其中的水果字母和名字分开。 喜欢这个:

 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发生了什么?

如果你拉(拉)一个真正的衣服拉链和一边,没有问题的左侧或右侧,牙齿比另一侧less,会发生什么? 那么拉链将停在那里。 Zip方法将完全相同:一旦到达任何一边的最后一项,它就会停止。 在我们的情况下,右侧的牙齿(食物名称)较less,因此它将在“甜甜圈”停止。

Zip方法允许你“合并”两个不相关的序列,使用你的调用者的合并函数提供者。 MSDN上的例子实际上是很好的展示你可以用Zip做什么。 在这个例子中,你需要两个任意的,不相关的序列,并且使用一个任意的函数(在这种情况下,把两个序列中的项目连接成一个string)进行合并。

 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 

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

如果我想要继续使用一个列表中的元素,那么该怎么办? 在这种情况下,较短的列表元素应该采用默认值。 在这种情况下输出为A1,B2,C3,D0,E0。 – 梁15年11月19日在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保留?

如果不知道哪个序列是较短的序列,则可以创build一个函数来进行猜测:

 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()); } 

plain.Zip()与ZipDefault()一起输出:

 A1 A1 B2 B2 C3 C3 D0 E0 

回到原始问题的主要答案 ,人们可能希望做的另一件有趣的事情(当被“压缩”的序列的长度不同时)是以这样的方式join它们,以便列表的结尾匹配而不是顶部。 这可以通过使用.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 
 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