【问题标题】:Does C# 7 have array/enumerable destructuring?C# 7 是否具有数组/可枚举解构?
【发布时间】:2018-05-28 16:17:54
【问题描述】:

在 JavaScript ES6 中,您可以像这样解构数组:

const [a,b,...rest] = someArray;

其中a 是数组中的第一个元素,b 是第二个元素,rest 是一个包含其余元素的数组。

我知道在 C#7 中您可以在赋值期间解构元组,但找不到与像这样解构数组/枚举相关的任何内容:

var (a,b) = someTuple;

我有一个IEnumerable,我需要第一个和第二个元素作为变量,我需要其余元素作为另一个 IEnumerable。我有一个解决方案,但觉得解构看起来会更干净。

【问题讨论】:

  • 将其余部分作为IEnumerable<T> 是很棘手的,因为为了获得第一个和第二个元素,您需要开始迭代。您可以将生成的IEnumerator<T> 包装在IEnumerable<T> 中,但您只能对其进行一次迭代。如果您不介意将其复制到List<T>,那会起作用,但效率很低...
  • (用数组来做这一切会简单得多。)
  • 你可以使用Succinc<T> library(免责声明:我写的)。它提供了IEnumerable&lt;T&gt; 的解构(没有@JonSkeet 提到的重新枚举问题)。你的代码就是var (a, (b, rest)) = someArray;
  • @DavidArno 我认为您应该将此作为答案发布,因为语法与 OP 想要的非常接近。如果可以提取此功能并发布它的代码,那就更好了。
  • @Evk,好的,我已将其添加为答案。

标签: c# destructuring c#-7.0


【解决方案1】:

我试图让它更短,性能更有效。所以我避免调用IEnumerable&lt;T&gt;.ToList() 方法,如果我们有一个大列表,这将是昂贵的。

public static void Deconstruct<T>(this IEnumerable<T> list, out T first, out IEnumerable<T> rest) {
    first = list.FirstOrDefault();
    rest = list.Skip(1);
}

public static void Deconstruct<T>(this IEnumerable<T> list, out T first, out T second, out IEnumerable<T> rest) {
    first = list.FirstOrDefault();
    (second, rest) = list.Skip(1);
}

public static void Deconstruct<T>(this IEnumerable<T> list, out T first, out T second, out T third, out IEnumerable<T> rest) {
    first = list.FirstOrDefault();
    (second, third, rest) = list.Skip(1);
}

【讨论】:

    【解决方案2】:

    事实证明,不仅可以解构元组,还可以解构任何具有 Deconstruct 静态(或扩展)方法和匹配签名的类型。为IEnumerable 正确解构并非易事(参见 David Arno in this answer 建议的库),所以让我们看看它是如何与简单的IList 一起工作的(实现无关紧要,这是一个例子,当然可以更好/不同):

    public static class Extensions {
        public static void Deconstruct<T>(this IList<T> list, out T first, out IList<T> rest) {
            first = list.Count > 0 ? list[0] : default(T); // or throw
            rest = list.Skip(1).ToList();
        }
    
        public static void Deconstruct<T>(this IList<T> list, out T first, out T second, out IList<T> rest) {
            first = list.Count > 0 ? list[0] : default(T); // or throw
            second = list.Count > 1 ? list[1] : default(T); // or throw
            rest = list.Skip(2).ToList();
        }
    }
    

    然后(在必要时添加相关的 using 语句后)您可以完全使用您想要的语法:

    var list = new [] {1,2,3,4};
    var (a,rest) = list;
    var (b,c,rest2) = list;
    

    或者你可以像这样链接解构(因为最后一个返回的值本身可以被解构):

     var (a, (b, (c, rest))) = list;
    

    在上一个版本中,您可以使用单个 Deconstruct 方法(返回第一个项目和其余项目的方法)解构为任意数量的项目。

    对于 IEnumerables 的实际用法,我建议不要重新实现轮子并使用提到的 David Arno 的库 in this answer

    【讨论】:

    • 对于那些不像OP,不关心rest的人,你可以使用discardvar (a, (b, (c, _))) = list;
    【解决方案3】:

    如果您想处理无限流,则需要稍微小心,例如来自 while(true) yield return 块。在这种情况下,您实际上无法检查流的长度以确保您有足够的项目来填充请求的元组。

    如果您的源实际上是无限的,则上述方法的组合将起作用 - 而不是计算 IEnumerable&lt;T&gt; 的长度,只需检查它是否有任何内容,然后按照术语实现多参数重载单参数重载:

        public static void Deconstruct<T>(this IEnumerable<T> list, out T head, out IEnumerable<T> tail)
        {
            head = list.First(); // throws InvalidOperationException for empty list
    
            tail = list.Skip(1);
        }
    
        public static void Deconstruct<T>(this IEnumerable<T> list, out T head, out T next, out IEnumerable<T> tail)
        {
            head = list.First();
            
            (next, tail) = list.Skip(1);
        }
    

    关键问题是当流用完时您希望发生什么。上面的代码会抛出一个InvalidOperationException。返回 default&lt;T&gt; 可能不是您想要的。在功能性上下文中,您通常会执行 cons,并将流拆分为单个头部和流尾部 - 然后在 cons 实现之外检查空流(因此在 Deconstruct 方法之外) .

    【讨论】:

      【解决方案4】:

      如果您想要一个与 C# 语言功能完全集成的解决方案,请使用 Evk's answer,它隐藏了一些实现细节。如果你不关心这个,你可以使用任何一个答案。


      据我所知,没有。但是,制作类似的东西并不难。

      这样的扩展方法怎么样:

      public static class EX
      {
          public static void Deconstruct<T>(this T[] items, out T t0)
          {
              t0 = items.Length > 0 ? items[0] : default(T);
          }
      
          public static void Deconstruct<T>(this T[] items, out T t0, out T t1)
          {
              t0 = items.Length > 0 ? items[0] : default(T);
              t1 = items.Length > 1 ? items[1] : default(T);
          }
      }
      

      你可以像这样使用它:

      int[] items = { 1, 2 };
      
      items.Deconstruct(out int t0);
      

      缺点是每个要返回的项目数都需要一个扩展方法。因此,如果您要返回多个变量,则此方法可能不是很有用。

      请注意,我没有检查长度和相关内容,但我猜你明白需要做什么。

      【讨论】:

      • 这可能对我有用,因为我可以将 IEnumerable 转换为数组。唯一缺少的是将其余项目作为新数组获取,但这很容易做到。
      • 不客气。您是否偶然投票并拒绝接受?还是不是你? @kmc059000
      • 我没有反对意见。我点错了接受。我喜欢这两个回答,所以正在等待正式接受。
      • “调整使用”是什么意思? @塞巴斯蒂安
      • 这是一个很酷的语言功能,而且确实是一个很好的答案。我不打算复制它。我的回答做同样的工作,但使用另一种语法,有时比必须知道确切的语言规则更容易理解。如果您认为这值得一票否决,那就这样吧。
      【解决方案5】:

      为了扩展其他贡献者暗示的解决方案,我提供了一个使用 IEnumerable 的答案。它可能没有经过优化,但效果很好。

      public static class IEnumerableExt
      {
          public static void Deconstruct<T>(this IEnumerable<T> seq, out T first, out IEnumerable<T> rest)
          {
              first = seq.FirstOrDefault();
              rest = seq.Skip(1);
          }
      
          public static void Deconstruct<T>(this IEnumerable<T> seq, out T first, out T second, out IEnumerable<T> rest)
              => (first, (second, rest)) = seq;
      
          public static void Deconstruct<T>(this IEnumerable<T> seq, out T first, out T second, out T third, out IEnumerable<T> rest)
              => (first, second, (third, rest)) = seq;
      
          public static void Deconstruct<T>(this IEnumerable<T> seq, out T first, out T second, out T third, out T fourth, out IEnumerable<T> rest)
              => (first, second, third, (fourth, rest)) = seq;
      
          public static void Deconstruct<T>(this IEnumerable<T> seq, out T first, out T second, out T third, out T fourth, out T fifth, out IEnumerable<T> rest)
              => (first, second, third, fourth, (fifth, rest)) = seq;
      }
      

      然后像这样使用这些解构器:

      var list = new[] { 1, 2, 3, 4 };
      var (a, b, rest1) = list;
      var (c, d, e, f, rest2) = rest1;
      Console.WriteLine($"{a} {b} {c} {d} {e} {f} {rest2.Any()}");
      // Output: 1 2 3 4 0 0 False
      

      【讨论】:

        【解决方案6】:

        C# 中,您需要自己编写,就像我正在使用的这个:

        public static class ArrayExtensions
            {
                public static void Deconstruct<T>(this T[] array, out T first, out T[] rest)
                {
                    first = array.Length > 0 ? array[0] : default(T);
                    rest = array.Skip(1).ToArray();
                }
        
                public static void Deconstruct<T>(this T[] array, out T first, out T second, out T[] rest)
                    => (first, (second, rest)) = array;
        
                public static void Deconstruct<T>(this T[] array, out T first, out T second, out T third, out T[] rest)
                    => (first, second, (third, rest)) = array;
        
                public static void Deconstruct<T>(this T[] array, out T first, out T second, out T third, out T fourth, out T[] rest)
                    => (first, second, third, (fourth, rest)) = array;
        
                public static void Deconstruct<T>(this T[] array, out T first, out T second, out T third, out T fourth, out T fifth, out T[] rest)
                    => (first, second, third, fourth, (fifth, rest)) = array;
        
        // .. etc.
            }
        

        然后简单地做:

        var (first, second,_ , rest) = new[] { 1, 2, 3, 4 }
        

        【讨论】:

        • 请注意,您在每个解构步骤中几乎都制作了数组的完整副本,使得解构有效 O(N^2),因此如果数组很大,使用此方法可能会很慢.
        【解决方案7】:

        您所描述的内容在函数式语言中通常称为“缺点”,通常采用以下形式:

        let head :: tail = someCollection
        

        I did propose this be added to C#,但没有收到非常好的反馈。所以我自己写了,你可以通过Succinc<T> nuget package使用。

        它使用解构来实现任何IEnumerable&lt;T&gt; 的头部和尾部的分裂。解构可以嵌套,因此您可以使用它一次性提取多个元素:

        var (a, (b, rest)) = someArray;
        

        这可能会提供您所追求的功能。

        【讨论】:

          【解决方案8】:

          语言中没有特殊的语法。

          不过,您可以利用元组语法来实现这一点

          class Program
          {
              static void Main(string[] args)
              {
                  int[] ints = new[] { 1, 2, 3 };
          
                  var (first, second, rest) = ints.Destruct2();
              }
          }
          
          public static class Extensions
          {
              public static (T first, T[] rest) Desctruct1<T>(this T[] items)
              {
                  return (items[0], items.Skip(1).ToArray());
              }
          
              public static (T first, T second, T[] rest) Destruct2<T>(this T[] items)
              {
                  return (items[0], items[1], items.Skip(2).ToArray());
              }
          }
          

          (在用于生产代码之前,应该对明显的错误场景进行错误处理)。

          【讨论】:

            【解决方案9】:

            真的很快:没有。

            C# 尚不支持对数组进行解构。

            目前,我在路线图上也找不到任何相关信息。在我们默认获得这个语法糖之前,似乎需要等待很多。

            正如@Nekeniehl 在 cmets 中添加的那样,它可以实现:gist.github.com/waf/280152ab42aa92a85b79d6dbc812e68a

            【讨论】:

            • 由于这个问题是关于C#本身的,我仍然认为我的回答是正确的。不过很高兴看到它可以添加。
            • 对不起,我不想说你的答案不对。其实是对的,目前还没有实现,只想指出可以实现=)
            • 是的,我也没有把它理解为批评,不用担心:)谢谢你的信息!
            猜你喜欢
            • 1970-01-01
            • 2021-12-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-03-22
            • 2014-09-19
            • 2017-05-26
            相关资源
            最近更新 更多