C # 7具有数组/可枚举结构吗?

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

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

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

我知道在 C # 7中可以在赋值过程中解构元组,但是找不到任何与解构数组/可枚举数有关的东西,比如:

var (a,b) = someTuple;

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

33330 次浏览

Really quick: No.

C# does not support destructuring for Arrays yet.

Currently, I cannot find any information of this on the roadmap, either. Seems like there will be a lot of waiting involved until we get this syntactic sugar by default.

As @Nekeniehl added in the comments, it can be implemented though: gist.github.com/waf/280152ab42aa92a85b79d6dbc812e68a

If you want a solution that is fully integrated with the C# language features, use Evk's answer, which hides some of the implementation detail. If you don't care about that, you can use either of the answers.


To my knowledge there is not. However, it is not very hard to make something similar.

What about an extension method like this:

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

And you can use it like so:

int[] items = { 1, 2 };


items.Deconstruct(out int t0);

The drawback is that you need an extension method per number of items to return. So if you have more than a few variables to return, this method might not be very useful.

Note that I left out checking the length, and related stuff, but you understand what needs to be done I guess.

There is no special syntax for it in the language.

You could leverage the tuple syntax, though, to arrive at this

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

(which should be extended with error handling for obvious error scenarios before being used in production code).

It turns out not only tuples can be deconstructed but any type which has Deconstruct static (or extension) method with matching signature. Doing deconstruction correctly for IEnumerable is not trivial (see library suggested by David Arno in this answer), so let's see how it works with simple IList instead (implementation is irrelevant, this one is for example and of course can be 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();
}
}

Then (after adding relevant using statements if necessary) you can use exactly the syntax you want:

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

Or you can chain deconstruction like this (because last returned value can itself be deconstructed):

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

With last version, you can deconstruct to any number of items using single Deconstruct method (that one which returns first item and the rest).

For real usage for IEnumerables, I'd suggest to not reimplement the wheel and use David Arno's library mentioned in this answer.

What you are describing is generally known in functional languages as "cons", which often takes the form:

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.

It uses deconstruction to achieve that splitting of the head and tail of any IEnumerable<T>. Deconstructs can be nested, so you can use it to extract multiple elements in one go:

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

This potentially could provide the functionality you are after.

In C# you will need to write your own, like this one I'm using:

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.
}

Then simply do:

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

To extend the solutions hinted by other contributors, I provide an answer that uses IEnumerable. It might not be optimized but it works quite well.

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

Then just use these deconstructors like this:

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

You need to be slightly careful if you want to handle infinite streams, as from a while(true) yield return block for example. In that case, you can't practically check the length of the stream to make sure you have enough items to populate the requested tuple.

If your source is actually infinite a combination of the above approaches would work -- rather than counting the length of the IEnumerable<T>, just check that it has any content at all, and then implement the multi-parameter overloads in terms of the single-parameter overload:

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

The critical question is what you want to happen when the stream runs out. The code above will throw an InvalidOperationException. Returning default<T> might not be what you want instead. In a functional context you'd typically be doing a cons, and splitting the stream into a single head an stream tail - and then checking for empty streams outside of your cons implementation (so outside of the Deconstruct method).

I tried to make it more shorter and performance effective. So I avoided the calling of IEnumerable<T>.ToList() method, which would be expensive if we had a large list.

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

We have it in C# 11 now as part of list patterns.

Usage examples:

List<int> list = new() { 1, 2, 3 };


// Here array must contain exactly 3 elements to match
// taking first element, disregarding last 2 ("_" disregards a single element)
if (list is [var firstElm, _, _])
{
// Do stuff with firstElm
}


// Do stuff with firstElm here too


Or

public void SomeFunc(int[] arr)
{
// guard
if (arr is not [var a, var b, var c]) return;


// act
a = b - c;
}

Or

Queue<int[]> queue = new ();
queue.Enqueue(new int[] { 1, 2, 3 });


// Here matching an array with length >= 2
// and taking first 2 elements (".." matches and disregards 0 or more elements)
// Arrays with less then 2 elements won't match
while (queue.TryDequeue(out var arr) && arr is [var a, var b, ..])
{
// Do stuff with a & b
}

See language reference on topic for full info.

As what such code is lowered to.

Or more pattern examples like

[var a, .., var y, _] for first and (second to last) elements

[_, >= 0, .., >=0, var z] to check & assign simultaneously

[.., [.., var zz]] to match last element of last subarray


PS Atm using stuff mentioned in first 2 examples is plenty for me.

Would be nice to have optional elements, but this already saves a lot of boilerplate.