【问题标题】:What is the use of Enumerable.Zip extension method in Linq?Linq 中 Enumerable.Zip 扩展方法有什么用?
【发布时间】:2011-07-04 14:39:27
【问题描述】:

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

【问题讨论】:

标签: c# linq enumerable


【解决方案1】:

这里的很多答案都展示了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>。类似地,button 的采样 bool 值可以解释为像 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

【讨论】:

    【解决方案2】:

    我没有代表点可以在 cmets 部分发布,但要回答相关问题:

    如果我希望 zip 在一个列表用完元素的情况下继续运行怎么办?在 在这种情况下,较短的列表元素应采用默认值。输出 在这种情况下为 A1、B2、C3、D0、E0。 – 梁 2015 年 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() 有一个警告Redim Preserve in 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
    

    回到原始问题的主要答案,人们可能希望做的另一件有趣的事情(当要“压缩”的序列的长度不同时)是将它们加入这样的方式使得列表的 end 匹配而不是顶部。这可以通过使用 .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&lt;T&gt; Pad&lt;T&gt;(this IEnumerable&lt;T&gt; input, long minLength, T value = default(T)) { long numYielded = 0; foreach (T element in input) { yield return element; ++numYielded; } while (numYielded &lt; minLength) { yield return value; ++numYielded; } }
    • 似乎我不确定如何在评论中成功格式化代码...
    【解决方案3】:

    不要让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 方法的作用完全相同:一旦到达任一侧的最后一项,它将停止。在我们的例子中,右侧的牙齿(食物名称)较少,因此它将在“甜甜圈”处停止。

    【讨论】:

    • +1。是的,“Zip”这个名字一开始可能会让人困惑。也许“Interleave”或“Weave”会是该方法更具描述性的名称。
    • @bacon 是的,但是我将无法使用我的拉链示例;)我认为,一旦您弄清楚它就像拉链一样,之后就很简单了。
    【解决方案4】:

    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
    

    【讨论】:

    • 我喜欢这个答案,因为它显示了当元素数量不匹配时会发生什么,类似于msdn documentation
    • 如果我希望 zip 在一个列表中的元素用完的地方继续执行怎么办?在这种情况下,较短的列表元素应采用默认值。在这种情况下输出为 A1、B2、C3、D0、E0。
    • @liang 两种选择:A) 编写您自己的Zip 替代方案。 B) 写一个方法到yield return 较短列表的每个元素,然后无限期地继续yield returning default。 (选项 B 要求您提前知道哪个列表较短。)
    【解决方案5】:

    它遍历两个序列并将它们的元素一个接一个地组合成一个新的序列。所以你取序列 A 的一个元素,用序列 B 中的对应元素进行变换,结果形成序列 C 的一个元素。

    一种思考方式是,它类似于Select,除了它不是从单个集合中转换项目,而是一次对两个集合起作用。

    来自MSDN article on the method

    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的相似之处
    【解决方案6】:

    正如其他人所说,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 语句。

    【讨论】:

    • 太棒了,正是我要找的东西,zip 之后的位置和其他命令
    【解决方案7】:
    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
    

    【讨论】:

      【解决方案8】:

      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”很棒,现在我更有可能记住它。
      【解决方案9】:

      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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-10-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-29
        • 1970-01-01
        相关资源
        最近更新 更多