Roslyn: Bug? implicit operator of tuple

Created on 3 Jan 2017  路  12Comments  路  Source: dotnet/roslyn

An implicit operator from a type to a tuple type is not accepted in an assignment to an unnamed tuple.
Version: Visual Studio 2017 RC2.


public class Point
{
    public int X, Y;
    public static implicit operator (int x, int y) (Point p)
    {
        return (p.X, p.Y);
    }
    public static implicit operator int (Point p)
    {
        return p.X;
    }
}

  ...
  var point = new Point { X = 1, Y = 2 };
  int i = point; 
  // this works as expected

  (int x, int y) t = point;
  // works as expected

  (int x, int y) = point;
  // The compiler gives two errors here:
  // CS1061 'Point' does not contain a definition for 'Deconstruct' ....
  // CS8129 No Deconstruct instance or extension method was found for type 'Point', with 2 out parameters.
Area-Compilers New Language Feature - Tuples Question Resolution-Answered

All 12 comments

That's not an assignment to an "unnamed tuple", that's a deconstruction. I'd wager that the compiler doesn't consider an implicit cast to a tuple as an intermediate step when attempting to deconstruct an arbitrary type that doesn't have a Deconstruct method. If you add the following method it will work as expected:

public static void Deconstruct(out int x, out int y) {
    x = this.X;
    y = this.Y;
}

As @HaloFour says, you are mixing implicit conversions and deconstructions.

One small point, static shouldn't be used for the Deconstruct method:
cs public void Deconstruct(out int x, out int y) { x = this.X; y = this.Y; }

So for the line:
cs (int x, int y) t = point;
You are assigning point to an (int x, int y) tuple named t and eg the X value of the point can then be accessed via t.x.

For the line:
cs (int x, int y) = point;
You are declaring two variables, x and y, which are assigned via "deconstructing" point, ie it's just syntactic sugar for:
cs point.Deconstruct(out int x, out int y);

Of course there's grounds for arguing that deconstruction should use an implicit cast if it exists, but that's a feature enhancement, rather than I bug (I think).

@DavidArno

Oops, started out writing that as an extension method. My bad.

I'm aware that a deconstruct method will work here. But to me it's not obvious behavior.

    (int x, int y) t = // deconstruct does not work, implicit operator works
    (int x, int y) = // deconstruct works, implicit operator does not work

I suspect for most developers, adding the t here feels like a matter of naming your tuple, not a switch from a deconstruction to a declaration on the other side of the equals sign. And of course the logic behind it can be justified. But you would prefer to avoid having to explain it till the end of days...

It would be important for developers to understand that (int x, int y) and (int x, int y) t are very different things, so the distinction, I think, is quite important. It's also important to understand that "(un)named tuple" refers specifically to the tuple elements.

That isn't to say that deconstruction shouldn't consider an implicit tuple conversion if a Deconstruct method isn't found, but it'd be up to the LDM to decide if that's a worthwhile fallback.

@mharthoorn,

Explainations are still needed as the difference isn't just "a matter of naming your tuple"

````cs
(int x, int y) t =point; // declare and set variable t
int w = t.x;

...

(int x, int y) = point; // declare and set variables x and y
int w = x;

// use "var" and the difference becomes more apparent:
var (x, y) = point; // declare and set variables x and y
int w = x;
````

I think the (int x, int y) = point; notation is really confusing as it looks too much like a type declaration.

I think what will happen a lot here is that people use the unnamed tuple (the deconstruct) and at some point say: "oh I will refactor my code a bit. I will name this tuple t" and then realize that it won't work, for no apparent reason.

Maybe. Like many things that's something that the developer will have to learn. Deconstruction/conversion or not, (int x, int y) and (int x, int y) t are fundamentally different declarations.

@mharthoorn,

I think what will happen a lot here is that people use the unnamed tuple (the deconstruct) and at some point say: "oh I will refactor my code a bit. I will name this tuple t" and then realize that it won't work, for no apparent reason.

That's why ensuring the new deconstruction and tuple features are well documented from the outset. You don't have an "unnamed tuple", you have a deconstruction. The tuple syntax is used to deconstruct a type into a series of values.
var (x, y) = point; // there is no tuple here, just a deconstruction of point to x & y
If we start using the wrong terminology from the outset, confusion will indeed reign. If we emphasise the difference between deconstructions and tuples from the outset, then such naive "refactorings" are less likely to occur.

It's important that the difference between deconstructions and tuples is understood as the two syntaxes have quite different effects, even when used with tuple-returning methods:
````cs
private (int x, int y) aTuple() => (1, 2);
....
(int x, int y) t = aTuple(); // assigns (1, 2) to tuple variable t
var t2 = aTuple(); // assigns (1, 2) to tuple variable t2

(int x, int y) = aTuple(); // assigns 1 to int variable x and 2 to int variable y
var (x2, y2) = aTuple(); // assigns 1 to int variable x2 and 2 to int variable y2
````

The expression (int x, int y) isn't an lvalue, so the ordinary assignment expression (with the possible use of a user-defined conversion) isn't available in this context.

@gafter, could you put "The expression (int x, int y) isn't an lvalue" in to layman's terms, please? I thought it meant the left hand side of an assignment statement, but in that case it definitely can be used as an lvalue as the following code compiles and runs just fine:
````cs
[Test]
public void TestDeconstruct()
{
(int x, int y) = aTuple();
Assert.AreEqual(2, y);
}

private (int x, int y) aTuple() => (1, 2);
````
So I'm guessing it doesn't mean that?

What I mean is that the expression (int x, int y) doesn't designate a variable, or a location in memory, to which a value can be assigned. According to the language specification "The left operand of an assignment must be an expression classified as a variable, a property access, an indexer access, or an event access." It used to be that was a requirement of the assignment expression, but now the assignment expression has two forms. The old form has that restriction, and the new deconstruction form requires a tuple on the left-hand-side. They have separate rules for how they work.

Was this page helpful?
0 / 5 - 0 ratings