We resolved a number of questions related to tuples and deconstruction, and one around equality of floating point values in pattern matching.
For tuple element names occurring in partial type declarations, we will require the names to be the same.
partial class C : IEnumerable<(string name, int age)> { ... }
partial class C : IEnumerable<(string fullname, int)> { ... } // error: names must be specified and the same
For tuple element names in overridden signatures, and when identity convertible interfaces conflict, there are two camps:
- Strict: the clashes are disallowed, on the grounds that they probably represent programmer mistakes
- when overriding or implementing a method, tuple element names in parameter and return types must be preserved
- it is an error for the same generic interface to be inherited/implemented twice with identity convertible type arguments that have conflicting tuple element names
- Loose: the clashes are resolved, on the grounds that we shouldn't (and don't otherwise) dictate such things (e.g. with parameter names)
- when overriding or implementing a method, different tuple element names can be used, and on usage the ones from the most derived statically known type win, similar to parameter names
- when interfaces with different tuple element names coincide, the conflicting names are elided, similar to best common type.
interface I1 : IEnumerable<(int a, int b)> {}
interface I2 : IEnumerable<(int c, int d)> {}
interface I3 : I1, I2 {} // what comes out when you enumerate?
class C : I1 { public IEnumerator<(int e, int f)> GetEnumerator() {} } // what comes out when you enumerate?
We'll go with the strict approach, barring any challenges we find with it. We think helping folks stay on the straight and narrow here is the most helpful. If we discover that this is prohibitive for important scenarios we haven't though of, it will be possible to loosen the rules in later releases.
Should it be possible to deconstruct tuple literals directly, even if they don't have a "natural" type?
(string x, byte y, var z) = (null, 1, 2);
(string x, byte y) t = (null, 1);
Intuitively the former should work just like the latter, with the added ability to handle point-wise var
inference.
It should also work for deconstructing assignments:
string x;
byte y;
(x, y) = (null, 1);
(x, y) = (y, x); // swap!
It should all work. Even though there never observably is a physical tuple in existence (it can be thought of as a series of point-wise assignments), semantics should correspond to introducing a fake tuple type, then imposing it on the RHS.
This means that the evaluation order is "breadth first":
- Evaluate the LHS. That means evaluate each of the expressions inside of it one by one, left to right, to yield side effects and establish a storage location for each.
- Evaluate the RHS. That means evaluate each of the expressions inside of it one by one, left to right to yield side effects
- Convert each of the RHS expressions to the LHS types expected, one by one, left to right
- Assign each of the conversion results from 3 to the storage locations found in 1.
This approach ensures that you can use the feature for swapping variables (x, y) = (y, x);
!
(var x, var y) = GetTuple(); // works
(var x, var y) t = GetTuple(): // should it work?
No. We will keep var
as a thing to introduce local variables only, not members, elements or otherwise. For now at least.
We decided that deconstructing assignment should still be an expression. As a stop gap we said that its type could be void. This still grammatically allows code like this:
for (... ;; (current, next) = (next, next.Next)) { ... }
We'd like the result of such a deconstructing assignment to be a tuple, not void. This feels like a compatible change we can make later, and we are open to it not making it into C# 7.0, but longer term we think that the result of a deconstructing assignment should be a tuple. Of course a compiler should feel free to not actually construct that tuple in the overwhelming majority of cases where the result of the assignment expression is not used.
The normal semantics of assignment is that the result is the value of the LHS after assignment. With this in mind we will interpret the deconstruction in the LHS as a tuple: it will have the values and types of each of the variables in the LHS. It will not have element names. (If that is important, we could add a syntax for that later, but we don't think it is).
Deconstruction and conversion are similar in some ways - deconstruction feels a bit like a conversion to a tuple. Should those be unified somehow?
We think no. the existence of a Deconstruct
method should not imply conversion: implicit conversion should always be explicitly specified, because it comes with so many implications.
We could consider letting user defined implicit conversion imply Deconstruct
. It leads to some convenience, but makes for a less clean correspondence with consumption code.
Let's keep it separate. If you want a type to be both deconstructable and convertible to tuple, you need to specify both.
Should they implement Deconstruct
and ITuple
, and be convertible to tuples?
No. There are no really valuable scenarios for moving them forward. Wherever that may seem desirable, it seems tuples themselves would be a better solution.
We should allow deconstruction to feature wildcards, so you don't need to specify dummy variables.
The syntax for a wildcard is *
. This is an independent feature, and we realize it may be bumped to post 7.0.
pair += (1, 2);
No.
What equality should we use when switching on floats and doubles?
- We could use
==
- thencase NaN
wouldn't match anything. - We could use
.Equals
, which is similar except treatingNaN
s as equal.
The former struggles with "at what static type"? The latter is defined independently of that. The former would equate 1 and 1.0, as well as byte 1 and int 1 (if applied to non-floating types as well). The latter won't.
With the latter we'd feel free to optimize the boxing and call of Equals away with knowledge of the semantics.
Let's do .Equals
.