-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
C# Design Notes for Jul 13, 2016 #13015
Comments
🎉 Design notes! |
Will this be allowed? partial class C : IEnumerable<(string, int)> { ... }
partial class C : IEnumerable<(string, int)> { ... } |
@paulomorgado : Yes. This is just about what to do when parts disagree on tuple element names. |
What about LINQ query syntax? That will presumably keep using anonymous types (to maintain compatibility) and some LINQ provider might appreciate |
Agree with @svick. LINQ uses anonymous types, seems like there's a lot of backwards compatibility to think of here. I also don't think Tuples are always the hands down better solution either; the BCL team made Tuple a reference type to optimize for cases when it has lots of properties. Seems like reference types (a la anonymous types) still have a place. |
They could emit tuples for LINQ to objects but continue to emit anonymous types for queryable expression trees. That would solve compatibility issues but still provide better performance for in-memory operations. This would only apply to those query clauses that automatically involve a projection, like |
Queriables might rely on the property names. Although, other than the generated query, I wonder if that would make any difference. |
What's the reasoning behind not allowing compound assignment with distributive semantics? |
@timgoodman We don't need a reason for each of the billions of things we do not do. Rather we choose to do things on the basis of having a good reason to do them, and it being of sufficiently high customer value for the effort. |
@gafter That's fair. I just figured that since it's part of the meeting notes, that there may have been some discussion beyond just "No". |
Just repeating my comment on the C# 7.0 blog post regarding wildcards: Wild cards using a * look a little off to me. Is there any reason why null is not used instead? p.GetCoordinates(out int x, out int y); // Don’t ignore
p.GetCoordinates(out int x, null); // Ignore return
p.GetCoordinates(out int x, out null); // Alternative ignore return And in the cases where there are overloads: p.GetCoordinates(out int x, (int) null); // Ignore return for overloads
p.GetCoordinates(out int x, out (int) null); // Alternative ignore return for overloads Similarly: (var myX, *) = GetPoint();
(var myX, null) = GetPoint(); // Ignore |
Couldn't we simply omit the name? p.GetCoordinates(out int x, out); // No overloads
p.GetCoordinates(out int x, out int); // For overloads I'm sure I'm missing something. 😉 |
Fwiw I dislike the star and any keyword would make me happier. |
Note that wildcards would extend beyond |
@HaloFour so when I see
|
To reiterate what @jnm2 said above, |
That is exactly how the wildcard pattern works in functional languages. It can be used where you don't care about the result, for example: let (a, _) = tuple // don't care about the second tuple element These changes around tuples, deconstruction, type-switch and |
@HaloFour Not to switch viewpoints or anything, but we do have the existing paradigm of optional parameters where the placeholders are simply omitted. Why not consider out parameters as optional and follow the same style? It would also apply to the tuple example since tuples are the moral equivalent of parameter lists. |
@jnm2 That we do, but optional nature of the parameters are opt-in by the callee, not the caller. To have C# automatically start treating all |
@jnm2 that doesn't handle the case where the "don't care" or optional parameter is in the start or middle and not the end. Also I'm agreeing with others above, that's why I prefer null as it means "nothing" rather than * which means "all" or "everything". |
|
@Chris-GH Actually what I'm saying is that it does handle the start and middle cases via the comma, just like argument lists: Also Chris, I understand what HaloFour is saying. Don't think of |
now pattern matching is getting irrelevant. |
I think using a keyword/identifier instead of * for wildcard will work better. For example, Instead of would be in this case the keyword/identifier ignore is used to indicate that the developer is not interested in the myX parameter. Edit: I used the term keyword initially but as orthoxerox mentioned this can be an identifier. I'm fine with however we want to classify the term 'ignore'. My point is using it makes it clearer that the developer is not interested in the parameter value. |
@jstun What makes |
@jstun the point @orthoxerox was making wasn't about the term you used, it was an explanation of why your idea won't work. It wasn't a matter of "classification", but rather explaining why it can't (or rather shouldn't) be done. On the other hand, you cannot have an identifier named |
@dman2306 having * as a means for indicating you want to ignore a parameter is nonsense. Every developer is accustom to the * meaning any value, for example when searching someValue, means find me all that have someValue in the name. Now we're suggesting that * should be used to indicate ignore does not make sense to me and any other developer who pays attention to the code they write. Please look at the meaning of contextual keywords. You can use @orthoxerox link as well. ignore can be a contextual keyword in the example I provided. (var myX, ignore) = GetPoint(); Notice how I'm using ignore as the only value for the second parameter. I didn't specify an identifier. The compiler can easily infer that the developer is requesting that the parameter be ignored, see how clear that is. This will also satisfy those that would like to use ignore as an identifier. bool ignore = true; |
Deconstruction also uses the following syntax: var (myX, ignore) = GetPoint(); or: int myX, ignore;
(myX, ignore) = GetPoint(); or: int ignore;
(var myX, ignore) = GetPoint(); It is completely impossible for the C# compiler to know if |
In addition to the very reasonable objection that Some people might find it counterintuitive the first time they see it, but is anyone seriously going to be confused about it by the fifth time they see it? If not, this concern seems like much ado about (almost) nothing. |
Nobody mentioned * as already existing dereference operator in unsafe context. Is there really no potential ambiguity or confusion in this context? |
@HaloFour if deconstruction is using ignore as some sort of keyword then how can the compiler tell the difference when I declare a variable named ignore? @timgoodman if there's a single character to emphasize ignore wouldn't a more suitable character be the bang (!) symbol. (myX, !) = GetPoint(); |
That said, regarding This seems like it'd be a non-breaking change, since the code either doesn't compile or essentially does nothing today. And it has the (small) advantage of matching the syntax that F# and other languages use for the ignored value. I imagine that the compiler already optimizes away unaccessed local variables, so I guess the only change there is to supress the " Of course you'd also need to prevent the error on cases like
But if that's hard to do or is a breaking change for reasons I'm not seeing, I personally don't mind |
@stepanbenes I don't think the dereferencing operator could ever appear as the only token in the middle of either |
In both cases where It couldn't, which is why It's been suggested but it suffers the same problem as public class Foo {
private int _;
public void Bar() {
int x = 123;
// later
(x, _) = GetPoint(); // oops, overwrote the field _
}
} |
@HaloFour Mutable The experience is not that different from when you use |
@jnm Would it not be better to have something that can never be an identifier? eg Double Tilde (x,~~) = GetPoint(); If it consider different to an identifier, can also be highlighted differently in IDE. (eg greyed out) |
@HaloFour, well the compiler can tell if you never read the value from a variable, because it generates an "assigned to but never accessed" warning. So assuming it's a wildcard in that case seems harmless, even if for some odd reason your actual intent was to store the value in a variable and then never read it. I'm imagining this means initially parsing it as an identifier and only later saying "nope, it's a wildcard", and I really don't know if that would introduce some horrible complications. If there are existing optimizations to remove unaccessed variables, then maybe its enough to treat an undeclared One downside of that is you'd no longer produce an error when someone meant to type an existing identifier and somehow wrote And yes, as you point out, people could screw up if they forgot they had a variable named If this could be made to work for |
Even better. The compiler error preventing |
The wildcard syntax/pattern is being considered for a number of other scenarios aside deconstruction, including currently legal syntax where stuff.Select((_, index) => { ... }); or where the lambda is intended to represent a simple property accessor and the lambda argument itself is just noise: stuff.OrderBy(_=>_.FirstName); Using any legal identifier as the wildcard would immediately prevent supporting these scenarios with wildcards. |
@HaloFour That fits with what I'm saying. That experience is similar to what happens with even more frequency today, where I'll have something like I'd say the same experience would be nice for One of the benefits here is that it's completely backwards compatible with existing code. *(Yes, I know out wildcards aren't destructuring, but it's the same type of scenario.) |
@jnm2 So, with your proposal, there is no way to use wildcard as a lambda parameter and also inside the lambda? That doesn't sound like great design to me.
|
I don't think you understood me. I'm talking specifically about scenarios where wildcards are being considered that don't involve deconstruction. One of those scenarios is ignoring lambda arguments: // what does this mean? Are any of those identifiers in any scope?
stuff.Select((_, _) => { ... });
// should this be a compiler error for trying to shadow an existing variable, or a wildcard?
int _ = 123;
stuff.Select((_, index) => { ... });
// unambiguous
stuff.Select((*, *) => { ... }); If C# were to decide on |
From my point of view, in all but one case presented in this discussion the Point pt = window.Location;
if (pt is (0, void)) {
Console.WriteLine("The window is somewhere along the left side!");
} in contrast to having to scratch your head seeing all the other examples like: var (x, *) = window.Location;
if (x == 0) {
Console.WriteLine("The window is somewhere along the left side!");
} This looks way more intelligible to me: var (x, void) = window.Location; This would simply lead to expanding the meaning of
I think this will arrive much easier in the head of the average C# developer than having yet a third possible meaning for |
|
@falahati This is out of date. Further design discussions resulted in the adoption of |
Oh wow, so _ was possible after all. So happy C# ends up following the same notation used in F# and many other functional languages! 🎉 🎉 🎉 |
Just note that the compiler is forced to treat string _ = MethodThatReturnsString();
if (int.TryParse(s, out _)) { ... } // compiler error, _ is a variable of type string here And I hope you never name your fields In short, the compiler didn't solve the problems of using |
@HaloFour is still saying "over his dead body" :D |
@jnm2 Not my place to say that. I expressed my opinion and the team decided to go that route anyway. No biggie. But now there is the liability and onus to educate everyone on the behavior of this feature. |
Coming from F# it's nice that the adopted version of discard turned out to be _, but reading through @HaloFour's arguments made me think that maybe * would've been a better choice for C#. Oh well 😄. |
LDM notes for July 13 2016 are available at https:/dotnet/csharplang/blob/master/meetings/2016/LDM-2016-07-13.md |
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.
For tuple element names in overridden signatures, and when identity convertible interfaces conflict, there are two camps:
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?
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:
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":
This approach ensures that you can use the feature for swapping variables
(x, y) = (y, x);
!var in tuple types?
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:
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 deconstructible and convertible to tuple, you need to specify both.
Anonymous types
Should they implement
Deconstruct
andITuple
, 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
No.
Switch on double
What equality should we use when switching on floats and doubles?
==
- thencase NaN
wouldn't match anything..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
.The text was updated successfully, but these errors were encountered: