csharplang/meetings/2016/LDM-2016-07-13.md
2018-01-25 13:11:49 -08:00

6.5 KiB

C# Language Design Notes for Jul 13, 2016

Agenda

We resolved a number of questions related to tuples and deconstruction, and one around equality of floating point values in pattern matching.

Handling conflicting element names across type declarations

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:

  1. 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
  2. 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.

Deconstruction of tuple literals

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":

  1. 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.
  2. Evaluate the RHS. That means evaluate each of the expressions inside of it one by one, left to right to yield side effects
  3. Convert each of the RHS expressions to the LHS types expected, one by one, left to right
  4. 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 in tuple types?

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

Void as result of deconstructing assignment?

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 as conversion and vice versa

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.

Anonymous types

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.

Wildcards in deconstruction

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.

compound assignment with distributive semantics

pair += (1, 2);

No.

Switch on double

What equality should we use when switching on floats and doubles?

  • We could use == - then case NaN wouldn't match anything.
  • We could use .Equals, which is similar except treating NaNs 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.