Deconstruction in a LINQ select expression

Viewed 642

Given an object with a Deconstruct method like the following:

record Point(int X, int Y);

var point = new Point(1, 2);
var (x, y) = point;

Console.WriteLine(x); // 1
Console.WriteLine(y); // 2

Is it possible to deconstruct the object's values in a LINQ select statement?

For example rather than:

points.Select(p => p.X + p.Y)

this

// CS0019 Operator '+' cannot be applied to operands of type 'UserQuery.Point' and 'int'
points.Select((x, y) => x + y)

This causes a compilation error as it's using the Select method overload that takes a Func<Point, int>

4 Answers

Here is how I would do it without the extension method, as long as my point object can be deconstructed as shown in your question, I can do the following:

points.Select(p =>
{
    // deconstruct each element of the object
    var (x, y) = p;
    return x + y;
});

in one line

points.Select(p => { var (x, y) = p; return x + y; });

I hope it can solve your question

In the default library Select extension method allows being used by IEnumerable collection objects.

The method signature looks like below.

public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

If you want to deconstruction as select expression you can try to write your custom Select extension method.

public static class PointExt
{
    public static IEnumerable<T> Select<T>(this IEnumerable<Point> points, Func<int, int, T> selector)
    {
        foreach (var p in points)
        {
            yield return selector(p.X, p.Y);
        }
    }

    public static T Select<T>(this Point p, Func<int, int, T> selector)
    {
        return selector(p.X, p.Y);
    }
}

Then it can use as this code snippet.

public static void Main()
{
    var point = new Point(1, 2);
    List<Point> points = new List<Point>(){ point };
    var res = points.Select((x, y) => x + y);
    Console.WriteLine(res.First());
    Console.WriteLine(point.Select((x, y) => x + y));
}

c# online

Out of the box, no, what you're asking isn't possible.

There is more than one reason.

Reason #1

By default, there's an Enumerable.Select method that has two parameters. One of Type T (the enumerable member) and one of type int (the index). This reason is why you receive:

// CS0019 Operator '+' cannot be applied to operands of type 'UserQuery.Point' and 'int'

Reason #2

The second reason this isn't possible is the notion of deconstruction doesn't apply to parameter creation.

For instance:

var (x, y) = myPoint;

Is different than the lambda syntax of:

var myPointSumQuery = myPoints.Select((x, y) => x + y);

Conclusion

There's no built-in way for the compiler to know 'Hey they want to deconstuct this enumerable series into parameters'.

You get close with the following:

var p = new Point(3, 3);
var p2 = new[] { p };
var p3 = p2.Select(x => (x.X, x.Y)).Select(((int x, int y) e) => e.x + e.y);

But if you notice, you end up with the same problem of X and Y needing to be contained within the parameter 'e'.

There was discussion along these lines which would get you closer if you see the following link: Proposal: Tuple deconstruction in lambda argument list

It seems that it was never implemented, because this is illegal:

var p3 = p2.Select(x => (x.X, x.Y)).Select(((int x, int y)) => x + y);

Since C# cannot deconstruct tuples into parameters, the extended notion of 'Deconstruct the each point's members into parameters' would not work, either.

May be you can create some extension method for point, which looks similar to Select in LINQ.

public static class PointExtension
{
    public static IEnumerable<TResult> Select<TResult>(this IEnumerable<Point> source, Func<int, int, TResult> selector)
    {
        foreach (var item in source)
        {
            yield return selector(item.X, item.Y);
        }
    }
}

Now you can call the extension method as you mentioned. points.Select((x, y) => x + y)

Related