C# 7 有 array/enumerable 解构吗?

Does C# 7 have array/enumerable destructuring?

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

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

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

我知道在 C#7 中你可以在赋值期间解构元组,但找不到任何与解构相关的东西 arrays/enumerables 像这样:

var (a,b) = someTuple;

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

真快:没有

C# 尚不支持数组解构。

目前,我在路线图上也找不到这方面的任何信息。在我们默认获得这个语法糖之前,似乎会有很多等待。

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

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


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

像这样的扩展方法怎么样:

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

缺点是每个项目数需要一个扩展方法到 return。所以如果你有多个变量要return,这个方法可能不是很有用。

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

语言中没有针对它的特殊语法。

不过,您可以利用元组语法来达到此目的

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

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

事实证明,不仅元组可以被解构,任何具有 Deconstruct 静态(或扩展)方法和匹配签名的类型都可以被解构。对 IEnumerable 进行正确的解构并非易事(请参阅 David Arno 建议的库),所以让我们看看它如何使用简单的 IList 代替(实现无关紧要,例如这个)当然可以 better/different):

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 方法(即 returns 第一项和其余项)解构任意数量的项目。

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

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

let head :: tail = someCollection

I did propose this be added to C#, but it didn't receive very favourable feedback. So I wrote my own, which you can use via the Succinc<T> nuget package.

它使用解构来实现任何IEnumerable<T>的头部和尾部的分裂。解构可以嵌套,所以你可以用它一次性提取多个元素:

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

这可能会提供您想要的功能。

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 }

为了扩展其他贡献者暗示的解决方案,我提供了一个使用 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

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

如果您的来源实际上是无限的,则可以结合使用上述方法——而不是计算 IEnumerable<T> 的长度,只需检查它是否包含任何内容,然后实现 multi-parameter 重载 single-parameter 重载:

    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<T> 可能不是您想要的。在功能上下文中,您通常会执行 cons,并将流拆分为单个流头和流尾 - 然后检查 cons 实现之外的空流(因此在 Deconstruct方法)。

我试图让它更短并且性能更有效。所以我避免了调用 IEnumerable<T>.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);
}