In this meeting we took a look at what the scope rules should be for variables introduced by patterns and out vars.
So far in C#, local variables have only been:
for
, foreach
and using
statements are all able to introduce locals, but at the same time also constitute their scope. Declaration statements can introduce local variables into their immediate surroundings, but those surroundings are prevented by grammar from being anything other than a block {...}
. So for most statement forms, questions of scope are irrelevant.
Well, not anymore! Expressions can now contain patterns and out arguments that introduce fresh variables. For any statement form that can contain expressions, we therefore need to decide how it relates to the scope of such variables.
Our default approach has been fairly restrictive:
if
statement aren't in scope in the else
clause (to allow reuse of variable names in nested else if
s)This approach caters to the "positive" scenarios of is
expressions with patterns and invocations of Try...
style methods with out parameters:
``` c#
if (o is bool b) ...b...; // b is in scope
else if (o is byte b) ...b...; // fine because bool b is out of scope
...; // no b's in scope here
It doesn't handle unconditional uses of out vars, though:
``` c#
GetCoordinates(out var x, out var y);
...; // x and y not in scope :-(
It also fits poorly with the "negative" scenarios embodied by what is sometimes called the "bouncer pattern", where a method body starts out with a bunch of tests (of parameters etc.) and jumps out if the tests fail. At the end of the test you can write code at the highest level of indentation that can assume that all the tests succeeded:
void M(object o)
{
if (o == null) throw new ArgumentNullException(nameof(o));
...; // o is known not to be null
}
However, the strict scope rules above make it intractable to extend the bouncer pattern to use patterns and out vars:
``` c#
void M(object o)
{
if (!(o is int i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, but i is out of scope :-(
}
## Guard statements
In Swift, this scenario was found so important that it earned its own language feature, `guard`, that acts like an inverted `if`, except that a) variables introduced in the conditions are in scope after the `guard` statement, and b) there must be an `else` branch that leaves the enclosing scope. In C# it might look something like this:
``` c#
void M(object o)
{
guard (o is int i) else throw new ArgumentException("Not an int", nameof(o)); // else must leave scope
...i...; // i is in scope because the guard statement is specially leaky
}
A new statement form seems like a heavy price to pay. And a guard statement wouldn't deal with non-error bouncers that correct the problem instead of bailing out:
``` c#
void M(object o)
{
if (!(o is int i)) i = 0;
...; // would be nice if i was in scope and definitely assigned here
}
(In the bouncer analogy I guess this is equivalent to the bouncer lending the non-conforming customer a tie instead of throwing them out for not wearing one).
## Looser scope rules
It would seem better to address the scenarios and avoid a new statement form by adopting more relaxed scoping rules for these variables.
_How_ relaxed, though?
**Option 1: Expression variables are only scoped by blocks**
This is as lenient as it gets. It would create some odd situations, though:
``` c#
for (int i = foo(out int j);;);
// j in scope but not i?
It seems that these new variables should at least be scoped by the same statements as old ones:
Option 2: Expression variables are scoped by blocks, for, foreach and using statements, just like other locals:
This seems more sane. However, it still leads to possibly confusing and rarely useful situations where a variable "bleeds" out many levels:
c#
if (...)
if (...)
if (o is int i) ...i...
...; // i is in scope but almost certainly not definitely assigned
It is unlikely that the inner if
intended i
to bleed out so aggressively, since it would almost certainly not be useful at the outer level, and would just pollute the scope.
One could say that this can easily be avoided by the guidance of using curly brace blocks in all branches and bodies, but it is unfortunate if that changes from being a style guideline to having semantic meaning.
Option 3: Expression variables are scoped by blocks, for, foreach and using statements, as well as all embedded statements:
What is meant by an embedded statement here, is one that is used as a nested statement in another statement - except inside a block. Thus the branches of an if
statement, the bodies of while
, foreach
, etc. would all be considered embedded.
The consequence is that variables would always escape the condition of an if
, but never its branches. It's as if you put curlies in all the places you were "supposed to".
While a little subtle, we will adopt option 3. It strikes a good balance:
Try
methods, as well as patterns and out vars in bouncer if-statements.It does mean that you will get more variables in scope than the current restrictive regime. This does not seem dangerous, because definite assignment analysis will prevent uninitialized use. However, it prevents the variable names from being reused, and leads to more names showing in completion lists. This seems like a reasonable tradeoff.
Yay, design notes!
Boo, changed scoping rules! I'm beating a dead horse, but I still think that it introduces inconsistencies into the language that aren't worth it. Why should an out
declaration in a while
leak but not an out
declaration in a for
? Even if the spec is written to be explicit and _technically_ correct I guarantee that it will still feel unintuitive and unexpected.
If we kept with the previous scoping rules for out
declaration variables there wouldn't need to be any new syntax introduced. We already have syntax to explicitly allow that variable to be defined in the enclosing scope:
int value;
if (int.TryParse(s, out value)) {
}
Now I do recognize that variable/type-switch patterns are a different case since they are implicitly readonly
and must be declared where they are assigned. I don't have a good answer to this, short of saying just treat the pattern variable like any run-of-the-mill out
variable (or use the out
keyword to assign that value to said variable). But I think that something better can be proposed than deciding that half of the loop statements do one thing and the other half do something completely different.
And, to reiterate, yay design notes! 🎉 🍻
The default should be that it is available in the surround scope. unless explicitly changed by the user.
eg
default
``` c#
int value;
if (int.TryParse(s, out value)) {
// value is in scope
} else
{
// value is in scope
}
// value is in scope
**local enclosing scope**
``` c#
if (int.TryParse(s, local out value)) {
// value is in scope
} else
{
// value is not in scope
}
// value is not in scope
``` c#
int value;
if (int.TryParse(s, local out value)) { // error value already exists.
// value is in scope
} else
{
// value is not in scope
}
// value is not in scope
using @MadsTorgersen example
``` c#
for (int i = foo(local out int j);;);
// j in scope
// i in scope
}
guard
c#
if (int.TryParse(s, guard out value)) {
// value isnot in scope
} else
{
// value is in scope
}
// value is not in scope
@AdamSpeight2008
I don't see how that is an improvement. It's still internally inconsistent, but now adds the complication of all of these new keywords. Can you apply local
or guard
to variables declared within a for
loop? Does that even make sense? I don't think so.
@HaloFour They are contextual only applying to out var
, saying which scope to introduce the variable into. The default makes sense as it simplifies the existing pattern.
```int value;
if( int.tryparse( text, out value )
If you're going change the scope the make it explicit rather than implicit.
@AdamSpeight2008, @HaloFour:
A principle that the new design manages to maintain is that when it comes to scopes, all local variables are the same. It doesn't matter how they are introduced, only _where_ they are introduced. This is very valuable in order for folks to build intuition: You can visualize the scopes as physical boundaries in the code.
It then merely becomes a question of "what establishes a scope". And yes, the weakest part of the chosen approach probably is the intuition around the fact that an if or while statement does not establish a scope for variables in its condition. Believe me, we had looong arguments around it! :-)
However, at the end of the day, the current proposal wins on having full expressiveness for the important scenarios, while having only _slightly_ surprising scopes.
@MadsTorgersen
I don't know, I imagine more complaints here about people not being able to reuse their variable names. Especially if they need to convert from a switch
statement to a series of if
/else
statements and all of a sudden, again, the rules change. Which is even weirder given that case labels don't introduce scope.
Believe me, we had looong arguments around it! :-)
Not the first time: https://roslyn.codeplex.com/discussions/565640
That's probably the most jarring aspect. This has been argued out before, long since settled, and all of a sudden we get this big 180. And as this decision came out of a particularly quiet time from the team it feels even more out of the blue.
I had a prior proposal (#10083) suggesting that semi-colons should be able to appear in the boolean-condition
part of an if
statement which gives the programmer explicit over which variables get to leak outside the condition expression. The proposal didn't support the "bouncer pattern" (because I think it's a bad idea for a mainstream programming language to leak scope like that. e.g. Pre-Standard C++'s for (int i = 0;
used to leak i
's scope to outside of the for loop and most people hated it) but it could be tweaked slightly to support it. i.e. the proposal could be modified to have the following semantics instead:
``` C#
if (foo(out int one)) {
} else {
}
// insert a dummy "if (false) {}" to suppress scope leakage outside the if chain
if (false) {
} else if (foo(out int two)) {
} else {
two; // two's scope ends here
}
// insert a dummy "if (true;" to suppress scope leakage to other branches
if (true; foo(out int three)) {
three; // three's scope ends here
} else {
}
one; // one's scope ends here
```
I'll mention it here as a way to enhance "Option 3" with granular control of how scopes are leaked.
The canonical examples always seem to be a bool return and an out. This is much better handled by an Option
if(foo() is Some x){
Console.WriteLine(x);
}else{
Console.WriteLine("Got nothing");
}
though I'm not sure if the current pattern matching proposal for C# can handle the above type of patterns.
The guard pattern as described above.
void M(object o)
{
guard (o is int i) else throw new ArgumentException("Not an int", nameof(o)); // else must leave scope
...i...; // i is in scope because the guard statement is specially leaky
}
again works better as an extension method on Nullable
T IfNull(this T? o, Action a){
if(o == null) a();
return o.Value;
}
used liked
void M(object o)
{
int i = (o as int ).IfNull(()=>throw new ArgumentException("Not an int", nameof(o));
}
and a language extension like ruby blocks would get rid of the lambda
T IfNull(this T? o, Block &a){
if(o == null) a();
return o.Value;
}
void M(object o)
{
int i = (o as int ).IfNull {
throw new ArgumentException("Not an int", nameof(o))
}
}
No need for weird scoping rules.
@HaloFour: Thanks for digging out the design notes where the previous design came from. We went back and looked at those arguments and found that we no longer believed in them. I acknowledge that this came "out of the blue" in the sense that it's a late design change; in fact (knock on wood) the last one for C# 7.0.
The fact is that we've been mulling it for a couple of months, but only had the extreme options on the table (the restrictive design vs. what is "Option 1" above). Both were really unacceptable. Only a couple of weeks ago did we come up with the compromise that is option 3. I think it retains the full upside of option 1 (supporting meaningful use of out vars in top level expression statements, supporting the bouncer pattern for patterns and out vars). At the same time it limits "variable spilling" to a reasonably intuitive set that are declared "near the top level".
Sometimes you want those variables in scope, sometimes you don't.
While this isn't a slam-dunk trade-off, in the end we think a and b are lesser evils than c and d.
@DerpMcDerp: It's an explicit goal _not_ to allow granular control of the scope of a given variable. A given construct should _be_ a scope, and all variables contained inside it are scoped by it, regardless of how they are declared.
(This principle is violated by dubious legacy behavior inside switch statements that unfortunately cannot be fixed in a non-breaking fashion. We'll consider this a wart and live with it).
@bradphelan: If we could start over we might have avoided the TryFoo(out ...)
pattern, or maybe even out parameters altogether. But the state of the world is that there are lots of out parameters out there, and as long as we're introducing variables in expressions anyway (with patterns) it seems good and right to use that to also improve the consumption experience of those out parameters. For Try
methods, you can now think of them almost as a way to write your own active pattern.
@madstorgesson yeah that's a nice way to think of out params as being
active pattern. Thanks for the insight.
On Fri, 5 Aug 2016, 18:20 Mads Torgersen, [email protected] wrote:
@HaloFour https://github.com/HaloFour: Thanks for digging out the
design notes where the previous design came from. We went back and looked
at those arguments and found that we no longer believed in them. I
acknowledge that this came "out of the blue" in the sense that it's a late
design change; in fact (knock on wood) the last one for C# 7.0.The fact is that we've been mulling it for a couple of months, but only
had the extreme options on the table (the restrictive design vs. what is
"Option 1" above). Both were really unacceptable. Only a couple of weeks
ago did we come up with the compromise that is option 3. I think it retains
the full upside of option 1 (supporting meaningful use of out vars in top
level expression statements, supporting the bouncer pattern for patterns
and out vars). At the same time it limits "variable spilling" to a
reasonably intuitive set that are declared "near the top level".Sometimes you want those variables in scope, sometimes you don't.
- If you don't and they are there anyway, what's the harm? a) some
variable names are taken and not available, and b) it may be a bit
confusing until you get the hang of it- If you do and they aren't there? c) Significant and clunky rewriting
of your code or even d) you have no way of using the new features.While this isn't a slam-dunk trade-off, in the end we think a and b are
lesser evils than c and d.@DerpMcDerp https://github.com/DerpMcDerp: It's an explicit goal _not_
to allow granular control of the scope of a given variable. A given
construct should _be_ a scope, and all variables contained inside it are
scoped by it, regardless of how they are declared.(This principle is violated by dubious legacy behavior inside switch
statements that unfortunately cannot be fixed in a non-breaking fashion.
We'll consider this a wart and live with it).@bradphelan https://github.com/bradphelan: If we could start over we
might have avoided the TryFoo(out ...) pattern, or maybe even out
parameters altogether. But the state of the world is that there are lots of
out parameters out there, and as long as we're introducing variables in
expressions anyway (with patterns) it seems good and right to use that to
also improve the consumption experience of those out parameters. For Try
methods, you can now think of them almost as a way to write your own active
pattern.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/dotnet/roslyn/issues/12939#issuecomment-237895131,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AABE8gP2mrMuByixMNg8ypD61ZypD_cKks5qc2LMgaJpZM4JdLzM
.
I'm confused how option 3 supports the bouncer pattern. In your example. isn't i still out of scope because of the implicit braces around the if branch?
``` C#
void M(object o)
{
if (!(o is int i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, but i is still out of scope with option 3 isn't it?
}
seems to be equivalent to:
``` C#
void M(object o)
{
if (!(o is int i))
{
throw new ArgumentException("Not an int", nameof(o));
}
...; // i is still out of scope due to block?
}
@mungojam Implicit braces around a branch affect patterns, declarations, etc. inside of the branch (between the implicit braces), the declaration inside of the condition part of the if statement continues to 'bleed'.
Since C# provides us with definite assignment analysis, I agree this sounds like a clean approach.
Thanks @Eyas, yes, I follow now, the variables are escaping to outside the if block, not just into each branch, and definite assignment analysis is doing its magic.
Sounds like a very reasonable compromise to me, in current code the variable had to be declared outside the if block anyway, e.g. when using Try...(out result)
This might be enough of an edge case for the bouncer pattern that it probably doesn't matter, but what happens if boolean operators that cause short circuiting are used in the conditional in the if block?
E.g.
if(dictionary != null && dictionary.TryGetValue("key", out int j)
{
//Stuff
}
// Is j in scope here? What happens if the dictionary was null?
@Shiney I understand that the variable j
will be in scope after if
, but it won't be definitely assigned.
@Shiney
It's no different than the following that you can write today:
int j;
if(dictionary != null && dictionary.TryGetValue("key", out j)
{
// j is in scope and is definitely assigned
}
// j is in scope but is not definitely assigned
@MadsTorgersen
What about the scope of out
declaration variables within LINQ queries? Allowing them to leak throughout the LINQ query would be very useful, like an automatic range variable. But if they leak to the enclosing scope they could potentially produce concurrency issues if the LINQ query is parallel.
I imagine that people will expect the following to "Just Work"™:
IEnumerable<string> strings = GetSomeStrings();
var query = from s in strings
where int.TryParse(s, out int i)
select i;
// is i in scope here?
@MadsTorgersen
Actually, thinking about it some more, specifically with pattern variables, the scope changes make little sense. As long as pattern variables remain implicitly readonly
, the scope is leaking into scopes where the identifier could never be definitely assigned. As such, it's just a wasted identifier.
@HaloFour doesn't seem wasted in general-- as you said in the different thread:
It seems much of the point of these scoping rules is to avoid the tons of extra syntax that Swift requires in order to influence the scope.
That seems worth having a wasted identifier _in some cases_, while allowing the identifier to be useful in the opposite case:
if (!x is Type t) throw new IllegalArugmentException(nameof(x));
// can still use t
Are you saying it is worth having special statements or special scoping rules just to get this set of use cases to work?
@Eyas
For the bouncer case it's probably fine. t
would be definitely assigned at least where you'd expect to use it. But in any other situation you'd be left with t
where 2 out of the 3 scopes where it exists it cannot be used nor can it be updated. At least out
variables can be updated so you can assign some fallback value.
@HaloFour and others,
I should clarify a few things that weren't in the notes above.
_Pattern variables are mutable_. For instance you can write:
c#
if (o is int i || (o is string s && int.TryParse(s, out i)))
{
// i is definitely assigned and o is either an int or a string with an int in it
}
There are a couple of things that also _already_ establish scopes, that I forgot to mention, because they aren't statements:
_lambda expressions already establish scopes_ to carry their parameters. These scopes also contain any variables defined inside the body of even expression bodied lambdas.
_Query clauses already establish scopes_ because query expressions are defined in terms of syntactic translation to lambda expressions which constitute scopes.
It is true that it would be nice to allow them the same scope as that of variables introduced with from and let clauses (which turn into separate lambda parameters for each clause in the translation). We don't know how to do that in practice, though.
_Catch clauses already establish scopes_ for the exception specifiers, and any filter expression on the catch clause is contained within that scope.
Hope that helps!
Mads
@HaloFour What would that query compile into? A combination of Select()
and Where()
, like this?
c#
var query = strings
.Select(s => { int i; bool res = int.TryParse(s, out i); return new { s, i, res }; })
.Where(x => x.res)
.Select(x => x.i);
I'm not sure this kind of translation would be expected. How the translation looks like is especially important for IQueryable
and custom LINQ providers.
@svick
In short, yes. The compiler could emit tuples to make it more efficient, which was/is on the board for LINQ expressions which project temporary anonymous types for range variables today like let
. The translation above should work fine with expression trees, although I agree that most queryable providers probably wouldn't understand it, but how many of them would understand methods with out
parameters today regardless of what you did with the scope?
Basically an extension of #6877 where the implementation concerns were already mentioned. But with let
seeming less and less necessary there likely wouldn't be another opportunity to add said functionality to LINQ, which means that pattern matching wouldn't have a place in stream processing, which in my opinion would be a pretty big fail.
I really think that it's a bad idea that a variable introduced in the condition of a if or while leak that statement.
In Try... methods, if the user wants the variable to leak, then just write the code the way it can be written today.
Patterns are another issue easily solved by using another variable declared outside the statement:
C#
int j;
if (o is int i || (o is string s && int.TryParse(s, out i)))
{
j = I;
}
else
{
j = -1;
}
// I should not be in scope here!!!
@paulomorgado The less variable declarations the more declarative the code is.....which leads to fewer bugs.
@tumtumtum, I'm sure you have hard data on that. Specially on cases like this one.
It's as if you put curlies in all the places you were "supposed to".
This is a good rule. I certainly would expect that the use of curlies in single-line if
branches would be purely a stylistic choice.
Although I guess it's not _quite_ true even today, since if (condition) { var x = 5; }
is legal (albeit pointless) but if (condition) var x = 5;
gives error CS1023
. Which is probably a good thing, since the programmer presumably meant something like int x; if(condition) x = 5;
With this change, will that still be an error?
In other words, do the invisible curlies apply _only_ to expression variables, but not to ordinary variable declarations in the same location? (If so, I'm not objecting to that small amount of inconsistency -- just curious.)
@paulomorgado
Falling back to old syntax always works. But then the usefulness of our var
would be much less, to an extent which I think not worth it.
Leaking a few variable names, in practice, does not increase risk of bug. And it does not make it harder to name variables, as long as the function is not too long. The only bad thing is it does not feel good. I think it is a good compromise.
I believe the best way to implement method contracts (#119) is to use some kind of guard statement instead of drowning method signature with oneliner checks (see https://github.com/dotnet/roslyn/issues/11308#issuecomment-219213073). So I'd go with guard statement here to do double duty in the future. As others said, it seems these scoping changes help in one case and get in the way in the rest.
@Kaelum
You can specify the type name as well, e.g. bool success = int.TryParse(text, out int result);
Sounds like style rules will allow you to enforce that var
can't be used in any situation where the variable could be explicitly defined. Some people like the terseness of var
. C++ has auto
. Java is likely getting var
, val
and/or let
. The trend is more implicit strong typing, and I mostly welcome it.
Preview 5 has only been out a few days, and I've been burned by the new out var
scoping rules already. It went something like this:
var text = "5.5";
if (int.TryParse(text, out var resultI4))
{
Console.WriteLine(resultI4);
}
else if (double.TryParse(text, out var resultR8))
{
Console.WriteLine(resultI4); // Typo!
}
This is not the pit of success I was hoping for.
@lorcanmooney
I would say that isn't so different from many other scenarios where that could happen given a typo, one being the way you would currently have to write it in C# where you'd have to declare both types before the if statement.
For me the trade-offs are worth it, and in your example that would hopefully raise a compiler warning for the unused resultR8 variable, although maybe it doesn't do that for out variables? Resharper hopefully would if you have that.
I would say that isn't so different from many other scenarios where that could happen given a typo...
You are of course correct, but my hope was that we could use out var
to reduce such problems.
@Kaelum
At first I was thinking like you - this is not good design. Scope is scope. Obey it.
Then I saw the 'out' var example, and took a step back. F.ex. how else could you both define a new variable and get it to live after a function call when ';' is encountered?
Yeah, it's a convenience feature, but if we consider it like this:
// explicit
int i;
blahblah(..., out i);
and
// implicit
blahblah(..., out int i);
I think it makes sense. You'd have to wrap both examples in {} to keep scope.
I'm not saying that I've yet come to terms with the thinking, but expressed like this it started to seem reasonable to me.
@Kaelum: Having spent some time letting this issue sink in, I think we're on the same page. Your final statement stands out, to the point I repeat it, in hopes someone in the group defining the language takes this to heart:
"As it stands, the rules are so convoluted, and polluted, its going to be impossible to know what variables are in scope."
Whether or not that is correct (though currently it seems not incorrect), just the fact it is claimed should be enough of a wake-up call.
I consider this a major, disruptive, language change, and as such it should not be allowed to go "live" unless all outstanding issues are addressed - and solved.
@Kaelum
Revert the rules to the previously proposed set and allow the developer to decide if they want the functionality that is being proposed here, by simply doing the following:
There is a downside to that. Now, as a developer i have to declare my variable before hand. This is a negative for me on two counts:
``` c#
Dictionary
return;
}
symbolWeightMap.yadda yadda
I would far prefer just writing:
``` c#
if (!somethingElse.TryGetSymbolWeights(out var symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
Indeed, as a primary proponent of the new scoping rules, i wanted very much to have a system whereby i could take _nearly_ all cases today where 'out' is used, and be able to switch them over to 'out var'.
The limited-scoping rules ended up being precisely that: very limiting.
As such, pulling the variable into the enclosing scope was both necessary for some cases (like the one i showed above), and (IMO) not that impactful for others.
To be clear, i recognize, and i'm cognizant of the desire to not have those variables be in scope. The examples given by many here were echoed internally and _absolutely_ have merit. The question was between:
In the end '2' won out. It was unpleasant to have a new feature that existing code couldn't move to naturally. Bog standard simple patterns (like guard clauses) were not possible, and would require an entirely new language construct to solve them. It seemed unfortunate that we'd add a feature like out-var, and would immediately need to introduce _another_ feature to make up for such limitations.
And, again, the complaints about this current scoping decision are _absolutely true_ and recognized. variables are put into a broader scope than _some_ would like. The difficulty is that there are two groups. One that wants the variables in the wider scope, and one that wants the variables in the narrower scope.
This is a case though where one approach would make the feature non-usable for some users (because the code would literally not compile), whereas the other makes the feature _less pleasant_ (but the feature is still available and works).
IMO, i'd rather us go with an approach that allows both groups to at least have a solution that uses this feature, as opposed to picking the narrower approach that is more ideal for one group, but which makes the feature literally not available for the other.
I _really_ wish there was a solution that would allow this _single_ language feature to be _great_ for both groups. One that allowed brevity and simple usage of var, while also making variables scoped mor widely for those who want that (like me), and one that allowed out-var to be used, but also be scoped _very_ narrowly for those who like that. But with the desire to only have a single feature, and to keep it simple (i.e. no new forms like wide out var whatever
), the decision was made to have somehting usable by both camps, even if not as ideal as possible for the 'narrow' camp.
Preview 5 has only been out a few days, and I've been burned by the new out var scoping rules already. It went something like this:
Yes. This is the unfortunate situation where there is no perfect solution. On the other hand, i've been able to update a bunch of code to use out-var in cases where i want wide-scoping. So the new feature has been a boon to me. Six of one, half dozen of another, and all that :-/
To those who do not want the variables in scope. Would you ok with an intellisense option (or code-style option) that flagged these wide-scope cases as problems in your code? You could then catch these cases where you unintentionally referred to a variable you would have preferred not be in scope.
I ask because i'm largely responsible for a lot of the intellisense/code-style type features in VS. As such, i'd be happy to put such a feature on our backlog for inclusion in the future. It's also something we'd likely welcome as a PR if any of you want to pick it up :)
@CyrusNajmabadi Alternatively we could allow variable shadowing with an option to flag them as warning. It's proposed before and has its own use cases specially with recursive patterns. To not let it get out of hand I think it'd help if we restrict which and where variables can be shadowed.
@CyrusNajmabadi
The scoping by itself is bad enough but what makes it absolutely horrible is the fact that the behavior is inconsistent. You get wide with if
but you get narrow with switch
. You get wide with while
but you get narrow with for
. It creates a slew of new rules that developers have to (and won't) remember in order to do their job and it will make refactoring a pain. Imagine just tweaking one loop and half of the rest of that function breaks?
What's worse is that the desire to make out
variables slightly easier to use now pollutes the entirety of pattern matching. Now it's impossible to use it in any meaningful way without spraying a ton of extra identifiers all over the place.
I'd rather have guard
and let
. Sure, that's a lot of new language constructs but they enabled the developer to explicitly declare their intent and achieve their goals without having to adopt old-JavaScript-style variable scoping.
Just to mention, it is possible to use let
with TryX
to widen var scope to the enclosing block; assuming previous scoping rules:
// match return value against a constant pattern
let true = TryX(out var result) else return;
// `result` in scope
Not as pleasant as guard
but the same functionality.
@CyrusNajmabadi
Would you ok with an intellisense option (or code-style option) that flagged these wide-scope cases as problems in your code?
No, that would not be OK, as the scope leakage problem would still be out there in the wild, and the genie can never be put back in the bottle. You say:
This is a case though where one approach would make the feature non-usable for some users (because the code would literally not compile), whereas the other makes the feature less pleasant (but the feature is still available and works).
In reality, the one approach would see a version of the C# compiler supporting scope leakage. People would use this and from then on, we'd all be stuck with it, due to the team's strict rule of avoiding breaking changes. No matter how unpopular this proves to be with the wider user-base, C# would, forever more, leak variable scope. The other approach would be to temporarily make out var
and is <type>
slightly less pleasant to use for some users whilst a proper solution is created for C# 7.1+.
Letting C# 7 ship with this problem would be a truly irresponsible act by the compiler team.
@CyrusNajmabadi,
I think the most frustrating thing about this scope leakage plan is that the use-cases for doing it are such weak ones. We have:
GetCoordinates(out var x, out var y);
// I want to access x & y here
which is fixed by using a little feature also added to C# 7, tuples:
var (x, y) = GetCoordinates();
// I can access x & y here, without scope leakage
And we have:
if (!(o is int i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, but i is out of scope :-(
which is fixed by using a ternary operator, and the new throw expression
:
var i = o is int j ? j : throw new ArgumentException("Not an int", nameof(o));
// i is in accessible here, again without scope leakage
// and, importantly, j is not in scope, as it's job is now done. Win, win situation!!!
Which leaves me wondering how the decision was ever made to make such a massive compromise to scoping for such poorly thought out reasons?
@DavidArno There is another one, if (!TryX(out var result)) return;
which guard
was proposed for.
@alrz,
Your own #14239 proposal could be a solution to that, by allowing return
to be an expression:
var result = TryX(out var x) ? x : return;
but that aside, really is enabling the saving of one line, by turning:
SomeType result;
if (!TyX(out result)) return;
into "your" version, really worth introducing a fundamental change to variable scope?
@DavidArno I'm not saying "yes" to your question but I think the whole point of out var
feature is to save that very line. Roslyn codebase uses this pattern _a lot_ so I'd expect this to be an important use case to deal with. I'd agree that this change is not the perfect solution though.
@CyrusNajmabadi
I'll be honest with you, I prefer this:
Dictionary<string, ImmutableArray<(ISymbol symbol, double matchWeight)>> symbolWeightMap;
if (!somethingElse.TryGetSymbolWeights(out symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
Over this:
if (!somethingElse.TryGetSymbolWeights(out var symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
Simply because the first has consistent scoping rules and the second doesn't, I don't like to trade readability for consistency and what I already know, most of the time.
If you REALLY want to solve the readability problem solve it at its root, allow var to be inferred based on the argument and then this would be completely possible.
var symbolWeightMap;
if (!somethingElse.TryGetSymbolWeights(out symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
And then allow people to do the following if they want a narrowed scoping rules:
if (!somethingElse.TryGetSymbolWeights(out var symbolWeightMap)) {
// Do something with symbolWeightMap
return;
}
In short do it _right_.
@alrz,
It would be really sad if the team were fundamentally damaging variable scoping rules just to save themselves a tiny bit of typing. :cry:
I've been kind of disappointed by the kind of arguments I'm hearing against this the scope changes which I think are fundamentally a good idea:
First, there is no formal contract between the return value of a function and its out
parameters. out
parameters are used for two patterns which are both, in a sense, obsolete:
TryXXX
methods for conditional returns. _Can use nullable return values instead._In either case, libraries that write methods using 1.
and 2.
will continue to exist. Therefore I'm not particularly convinced by @DavidArno's argument in this comment that we don't need to worry about the multiple return cases (the GetCoordinates
example).
Let me repeat. There is no formal contract between the return value of a function and its out
parameters. In fact, C# already forces us to define the value of out
parameters.
Second, for pattern variables, there is absolutely no problem or tradeoff. Scope is not the only thing that governs how you write your code. The C# compiler will fail with a compile error if it detects a use of a variable that is not definitely assigned.
guard
design patterns without introducing special syntax. In fact, the guard
-type checks on pattern variables in C# end up being much richer that a typical guard statement.Thus, the scoping rules are only potentially problematic for out
variables. My view:
out
variables _are_ definitely assigned and that is the only contract imposed by the language specification up till now.out var
declarations with the very specific design pattern of using out var
methods with bool-returning functions indicating success.Finally, with regards to this:
It would be really sad if the team were fundamentally damaging variable scope just to save themselves a tiny bit of typing.
I question that variable scope is being "damaged". Scope just tells you which variable names are available to you for use/reuse. In C#, scope is not damaged as easily due to definite assignment checks. Further, most advancements in programming languages (ever?) could be said to stem from "sav[ing] themselves a tiny bit of typing".
@Eyas What's wrong with my example? we get to have the same scoping rules and we get to type less and even less when we want a narrowed scope.
@Eyas,
I question that variable scope is being "damaged"
The following code compiles with the master branch:
var list = new List<int> { 1, 2, 3 };
while ((list is List<int> k))
{
break;
}
k = new List<int>();
The following doesn't:
var list = new List<int> { 1, 2, 3 };
foreach (var x in (list is List<int> k ? k : list))
{
break;
}
k = new List<int>(); // results in the error "The name 'k' does
// not exist in the current context"
Because, in the second case, k
exists just within the foreach
block, whereas in the first case, it leaks out of the while
block. I'm fascinated to know why you don't think this is damaged. Would you care to explain your thinking?
@DavidArno @Eyas
And don't forget:
object o = "123";
switch (o) {
case string value:
Console.WriteLine($"o is a string: \"{value}\"");
break;
case int value: // perfectly legal
Console.WriteLine($"o is an int: {value}");
break;
}
vs.
object o = "123";
if (o is string value) {
Console.WriteLine($"o is a string: \"{value}\"");
}
else if (o is int value) { // error CS0136, value already in scope
Console.WriteLine($"o is an int: {value}");
}
And how much messier will it be when case guards are added?
switch (o) {
case int i:
Console.WriteLine($"o is an int: {i}");
break;
case string s when int.TryParse(s, out int i): // legal? what will the scope of i be?
Console.WriteLine($"o is a string containing an int: {i}");
break;
case string s:
Console.WriteLine($"o is a string not containing an int: \"{s}\"");
break;
}
@DavidArno Different people weigh these issues differently :)
Clearly, you view the scope leakage as super bad, vs the other issues with tight scoping which are not so bad for you. Your POV is consistent and understandable. The question is still if it's the right thing to do.
Please understand that not everyone feels the same way and that there can be differing opinions which come down to personal biases toward what people care more or less about.
Simply because the first has consistent scoping rules and the second doesn't,
I _personally_ think the 'consistent scoping' concerns are being a little overblown. I honestly do. I think, in practice, people will use out-vars them, learn how they work, and just move on. There may be slight confusion at some point (just as there would be with narrow scoping rules), but it will be momentary. Users will update their mental model to understand, and then will move onto to solving whatever problem they've set out to solve.
@CyrusNajmabadi But in this case why can't we have it both ways? can you comment on my example?
No, that would not be OK, as the scope leakage problem would still be out there in the wild, and the genie can never be put back in the bottle.
That's fine. Thanks for sharing your opinion. I was wondering more for people who wanted this for their own codebases. i.e. similar to people who absolutely hate 'var' and want to make it so that it does not exist for their own projects, even if it's available for other users out there that are fine with it.
@eyalsk I will when i get into the office. Should be around 12-1 my time :)
@CyrusNajmabadi thanks!
Because, in the second case, k exists just within the foreach block, whereas in the first case, it leaks out of the while block. I'm fascinated to know why you don't think this is damaged. Would you care to explain your thinking?
When we discussed this ourselves, it was because while-loops have never brought anything into scope, and so the out-var naturally extended out to the more containing scope.
HOwever, 'foreach' loops _always_ have a scope that variables come into (which is super important for things like lambda captures). And it would be extremely strange for _everyone_ we talked to for an out-var in the foreach to go into a wider scope than the actual foreach variable.
The current proposal was the _least_ inconsistent of all approaches that also _enabled_ all code scenarios, and worked well with all existing APIs. Other approaches could claim things like "Scope purity" but came with other downsides that were felt to be too much of an issue.
To me, this was similar to how "nullable" compromised on things like "x >= y" not being the same as "x > y || x == y". There were no perfect solutions, but the one we picked satisfied the most areas we cared about. Users then learned about this little wart and were able to still use the feature effectively.
@CyrusNajmabadi,
That's fine. Thanks for sharing your opinion. I was wondering more for people who wanted this for their own codebases. i.e. similar to people who absolutely hate 'var' and want to make it so that it does not exist for their own projects, even if it's available for other users out there that are fine with it.
That is not a good analogy. I hate out var
, because out params are an anti-pattern in my view. I can use analysers to block their use though, so don't much care that they are being added. That is analogous to those that hate var
. Creating leaky scoping can't be worked around though. It will be inflicted on everyone to satisfy the few. That is irresponsible.
🍝 This is even more unprecedented in C# but what if the scope also followed definite assignment?
if (int.TryParse(s, out int i)) {
// i is in scope here
}
else {
// i is not in scope here
}
// i is not in scope here
if (!int.TryParse(s, out int j)) {
// j is not in scope here
throw new ArgumentException();
}
// j is in scope here
Good, bad or indifferent, what scares me the absolute most about this late change is how massively it impacts huge features going forward. You can look at both out var
and type-switch to be relatively small enhancements to the language, but all of pattern matching will suffer under these rules and it will be completely impossible to "fix" it if six months (or so) from now if this choice is realized to be a mistake. Given that all of these options are riddled with issues I'd rather see both out var
and type-switch punted until C# 8.0 to allow for more time to come up with ideas. Or maybe allow them only in experimental mode to allow for some real-world use outside of the Roslyn codebase.
@CyrusNajmabadi
To me, this was similar to how "nullable" compromised on things like "x >= y" not being the same as "x > y || x == y". There were no perfect solutions, but the one we picked satisfied the most areas we cared about. Users then learned about this little wart and were able to still use the feature effectively.
Yeah, I am one of those who would've insisted all comparison operations on nullables have return type bool?
and return null
when either argument is null
.
It creates a slew of new rules that developers have to (and won't) remember in order to do their job and it will make refactoring a pain. Imagine just tweaking one loop and half of the rest of that function breaks?
So, personally, i'm taking a pragmatic approach here. For example, i don't look at "while" and "foreach" and say "those should behave identically". I say "it makes sense to me that the out-var variables would go into the same scope as the loop variable". Or, alternatively "gosh, it feels weird that the out-var variable is not in the same scope as hte foreach variable" (if we kept the original proposal).
I feel the same way when i see:
c#
var x = Foo(out y);
It feels super weird to me that i would be declaring two variables, but putting them into separate scopes.
Note: i recognize that others feel _otherwise_. The purpose of my posts is to not say that those views are wrong. Or that they're not recognized. Or that they are not consistent. It's just to show that there are _differing_ opinions on how people feel about this.
And, given different opinions, a _looooooooooooooong_ discussion was had about what the choices were and what the various tradeoffs were. If there had been any options with 0 tradeoffs, we would have gladly gone with that. But, we went with what we felt was the best imperfect solution. :)
I think the most frustrating thing about this scope leakage plan is that the use-cases for doing it are such weak ones. We have:
which is fixed by using a ternary operator, and the new throw expression:
var i = o is int j ? j : throw new ArgumentException("Not an int", nameof(o));
@DavidArno First off, i definitely hear what you're saying. Your position is consistent and clear. It is based clearly off of certain tradeoffs that make sense given the weighting you give to them. And thank you very much for sharing and providing your voice here. It is super valuable and appreciated :)
Unfortunately, such weighting is simply not universally held. As an example of that, i find the the above code _extremely_ unpleasant and i doubt i would ever use it. Indeed, the distaste i have for that code is probably equal to the distaste you have about scoping :-/ And there's the problem, from your perspective, you see obvious solutions that weigh things more how you see it. But any such solutions have to have their pros/cons weighed against what we feel will be the issues for most users. In this case, trust me when i say that tons of perspectives and opinions were raised and you were not alone in your weighting.
In the end though, the group came down a different set of weights, even knowing that there would be some customers who would not like those choices.
Which leaves me wondering how the decision was ever made
The decision was made by actually talking to many developers, with combined man-centuries of development experience. THe different considerations were raised by all devs and were then evaluated and voted on by ever member of the language design team. Everyone discussed how they felt about all the different aspects of this, and it helped deeply inform a solution that could _in balance_ be very value-positive, despite having issues that not everyone was happy about.
@CyrusNajmabadi
For example, i don't look at "while" and "foreach" and say "those should behave identically".
Identically? No. Consistently? Yes. I'd say that it's not terribly uncommon for me to switch between while
and for
loops depending on the circumstances. Especially considering how flexible for
loops are with their conditions. Afterall, while (true)
is effectively equivalent to for(; true; )
.
And while C# is pretty far removed from its legacy it is worth noting that C does allow for variable declaration within the condition of a while
loop and in that language the scoping rules are the opposite of this decision:
while (int i = next()) {
cout << i << endl;
}
// i is not in scope here
I feel the same way when i see:
var x = Foo(out y);
It feels super weird to me that i would be declaring two variables, but putting them into separate scopes.
I actually agree with you there. Expressions don't create their own scope so it never felt right to me that the out
variable declaration would silently disappear into that scope.
The argument will probably be that while
conditions and if
conditions didn't create their own scope either. This is absolutely true, but I'd argue that they probably should.
I'll be honest with you, I prefer this:
``` C#
Dictionary
return;
}
symbolWeightMap.yadda yadda
Over this:
if (!somethingElse.TryGetSymbolWeights(out var symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
> Simply because the first has consistent scoping rules and the second doesn't
That's totally fair. But we leaned as an LDM toward supporting the latter because there was strong feedback that people wanted it. Note that 'consistent scoping rules' doesn't totally describe the situation IMO. The issue with scoping is that people view it in different ways, and 'consistency' is there or not depending on your POV. For example, here are things i think about:
1. There should be no semantic difference between a if-statement with a block with a single statement in it, versus an if-statement with a single statement in it. In that regard the new scoping is consistent with that goal.
2. Constructs that introduce variables should not introduce variables into different scopes. As such local-declaration-statements and foreach-statements should place their out-var vairables inot the same scope as the other variables they declared. In that regard the new scoping is consistent.
3. "out var" should be consistent with most cases of "var-decl + out" That means the variable will go into the scope that the "var-decl" was normally in. In that regard the new scoping is consistent.
4. We wanted out-vars and pattern variables to have consistent scoping. We felt it would be far more confusing to have two different expression that declared variables, with different scopes for each. IN that regard the new scoping is consistent.
5. This list is not exhaustive, just demonstrative.
So, you are _totally_ write that you can see inconsistencies. But, the converse is true as well. The _original_ proposal had other inconsistencies that also bugged people. Just that those inconsistencies bugged people more and seemd worse than the "inconsistencies" we ended up with.
> If you REALLY want to solve the readability problem solve it at its root, allow var to be inferred based on the argument and then this would be completely possible.
``` c#
var symbolWeightMap;
if (!somethingElse.TryGetSymbolWeights(out symbolWeightMap)) {
return;
}
symbolWeightMap.yadda yadda
And then allow people to do the following if they want a narrowed scoping rules:
if (!somethingElse.TryGetSymbolWeights(out var symbolWeightMap)) {
// Do something with symbolWeightMap
return;
}
IMO, this still doesn't solve hte problem. Because now i'm in the situation where 'out-var' code feels inconsistently scoped versus "ver-decl + out" code. You are _totally_ free to think that that's not an issue. Or that that's actually a _virtue_ :) ! I am, in _no way_ stating that your feelings here are incorrect or that your conclusions don't flow logically. I'm just pointing out that the feelings are simply not universal. Trust me that these views of yours were shared by others and were absolutely vocalized and considered as the LDM debated and voted on the topic. In the end though, other concerns won out. We went with an option that we felt had the best pro/con result of all the choices.
Thanks very much for your passion here and for providing continued valued insight on the different types of concerns out there :) We love it and it's great that you're willing to spend so much time and energy on something you clearly care deeply about. It's nice to see kindred spirits in the community that feel the same way about moving hte language forward as we do :)
One thing that I want to add to Cyrus' explanation, is that we wanted scope rules that work equally well for patterns and out variables. As many of you mention, having a broader scope for out variables _can_ be addressed simply by using an extra line to declare the variable, as today. Why inconvenient, it is possible.
However, for patterns you don't have that option. The pattern introduces the variable, and if you want that variable in the outer scope, there's nothing you can do. That's at the core of why we made the scopes the way we did. Again: we don't want different kinds of local variable declarations to lead to different scopes, so same rules have to apply to out variables and pattern variables. I happen to think that the rules are convenient for out variables also, but the reason they are _necessary_ is patterns.
Pattern variables do have one advantage over out variables, which is that they are only definitely assigned in the case of success. I wish we could do something similar with out variables, which would avoid the kind of copy/paste typo mentioned above by @lorcanmooney, but it's hard to do in a non-breaking way.
@CyrusNajmabadi
How much does it affect your coding style if while
retains its own scope like for
and foreach
? How about with ternary operations?
I'd probably be grudgingly satisfied with the scope changes if the two above constructs kept restricted scoping. I don't see much of a need of having the "leaky" scoping with while
loops at all. What are the scenarios that this enables? I look at ternary ops as being match
-lite, so following switch
restricted scoping I think would be more intuitive.
@CyrusNajmabadi, @MadsTorgersen Thank you very much for the explanation, I really appreciate it.
The argument will probably be that while conditions ... didn't create their own scope either. This is absolutely true, but I'd argue that they probably should.
First, i want to talk aobut "while". "while" is probably the construct we spent the least amount of time on. Mainly because _pragmatically_ we just don't think it will matter all that much in practice. We actualy went and looked at a ton of code, and i think there were practically no usages of "outs" in whiles. As such, it felt super minor when discussing all of this. We considered how someone would use "out" today in a "while". And as that variable would be in the outer-scope, we felt it was fine to maintain that.
I don't see much of a need of having the "leaky" scoping with while loops at all. What are the scenarios that this enables?
That's fair. But i also want to point out that "while" loops are probably one of the least important constructs to discuss here. In practice we are likely to see almost no usages of out-vars here. And even if we did, the fact that the variable goes into an outer scope is just so incredibly minor. And, if someone is using "out" in a while-expression today, they can move to "out-var" and maintain the semantics they have today. As such, the while-scoping seemed like an adequate solution to a very very very very minor part of the area we were trying to solve here. I'd very much _not_ like us to get bogged down on that case as i think the other cases "declaration-statement, if-statement, expression-statement, switch-statement and foreach" are _vastly_ more important. And, for those cases, i think we came up with a viable set of rules that enable the vast majority of scenarios we care about, with only limited inconvenience caused by scoping.
Now, let's get onto something several orders of magnitude more important: if-statements. :)
The argument will probably be that ... if conditions didn't create their own scope either. This is absolutely true, but I'd argue that they probably should.
This is one where there is just a very large split in opinion, with valid arguments going in either direction.
Here's how i ended up landing on things: i actually attempted to use the language in person projects that i have. And i found that when the variables were scoped just to the "if" that i got vastly less value out of them. It worked in some cases, but there were tons more than didn't work well for me. Furthermore, a prime thing that i wanted (to reduce the need for separated out declaration+initialization), wasn't addressed. In other words, out-var solved a very specific problem, just not all the problems i wanted it to solve for me.
As such, while part of me liked the idea that "if-statements create their own scope", my pragmatic side came down to this being just too limiting.
In the end, i holistically viewed the set of all those problems to be more important to address than just having a great solution for a specific problem. In other words, i openly and honestly went for a "jack of all trades" solution versus a "master of one" solution :) This was not an easy call for me. But it fit in,to what i think a large goal of C# is for me. Which is to have flexible solutions that fit a wide variety of use cases.
I hope this helps shed some light on this topic!
@eyalsk Thank you! I cannot express to you how amazing it is to be able to work on C# in this manner, with such a great group of passionate people who go out of their way to share their opinion with us and who help shape the language so much. I'm so glad we have this!
@CyrusNajmabadi @MadsTorgersen
We considered how someone would use "out" today in a "while". And as that variable would be in the outer-scope, we felt it was fine to maintain that.
Mind sharing those scenarios? I'd like to at least understand what swayed while
ending up in the "leaky" bucket.
I agree that the number of scenarios either way are likely low. Given such, without at least those scenarios, my opinion is that while
should fall under the "restricted" scope so that at least all of the looping constructs retain the same behavior. I think that consistency is important as at least a developer can quickly remember that rule.
However, do
/while
loops I can totally understand leaking considering the declaration follows the scope.
I'm just trying to wade through the opinions and feel out if there might be an option 3.5, something that enables your scenarios but also allows for @DavidArno and @Kaelum to write code in the style that they'd prefer without having to also deal with the leakiness unnecessary. On that I'd totally be willing to concede if
.
Finally, i'm likely going to bow out of this conversation (unless i feel it would be very helpful for me to pop back in). This whole topic has the chance to just go around and around and around (we've all been there and seen that happen when there's a lot of passion for opposing viewpoints) :)
I only came in to try to make it clear that we did care deeply about all the same points that many have brought up here. However, through a long and deep discussion on the topic, we ended in a place that we felt was still the best choice overall, despite having areas we knew would be rough for some (_including ourselves_) :)
My posts here are not to convince, just to help share knowledge and to allow everyone to understand all the different perspectives. Just as you've done that for me, i hope i can help out with that as well. And, as i have sooooo much work to do for RC and RTM, i def want to get on that asap.
BTW, i hope you get a chance to try out new features i've written for RTM like:
https://github.com/dotnet/roslyn/pull/14399 (helping code use expression-bodied members)
https://github.com/dotnet/roslyn/pull/14396 (helping code move to using patterns for as-expressions)
https://github.com/dotnet/roslyn/pull/14389 (helping code move to using patterns for is-expression)
https://github.com/dotnet/roslyn/pull/14383 (helping code move to using out-vars)
I'm super happy about so many of the features we've been able to add to C# as we move forward, and i'm committed to adding IDE features to help people use them effectively :)
We considered how someone would use "out" today in a "while". And as that variable would be in the outer-scope, we felt it was fine to maintain that.
Mind sharing those scenarios? I'd like to at least understand what swayed
while
ending up in the "leaky" bucket.
UGGGGGGGGGGGGGGGGGGGGH. I responded by acidentally editing your post. Github _thank goodness_ stopped me. But my entire answer was lost. So here goes again (just more brief as i don't have much time):
Here was the thinking:
If someone was using 'out' in a 'while' today then they were writing something like:
``` c#
while (... out x ...)
{
}
As such, with the rules of the langauge today, 'x' would be in a greater scope. As such, if that was acceptable for them, then it would be no worse if they moved ot:
``` c#
while (.. out var x ...)
{
}
and still had 'x' in the wider scope.
I think that consistency is important as at least a developer can quickly remember that rule.
Your post is totally fair and valid. However, as i pointed out above, it all depends on what sort of consistency is important to you. In your case, consistency of loops is important _and that's totally fair_. However, to me consistency that scoping of "out-var" be close to "var-decl + out" is also important.
To be brief (darn me for losing my original post): "while" is just very minor here. And i think the result we came up with is totally fine for nearly all cases. Yes, there might be a case where someone wants things slightly different. But i can live with that :)
I totally understand where people are coming from here. I love sinking my teeth into an issue. I love the nitty gritty. I have a deep streak for 'purity' in a language (as i see it). But at a certain point, i've discovered that coming up with pragmatic solutions tends to lead to very healthy outcomes. There was a reason i brought up the issue with "x >= y" vs. "x > y || x == y" before. It's still something that irks me when i'm coding. And yet, at the end of the day, it was not the grand mistake i thought it was at the time. It did not cause the collapse and downfall of romanC# civilization :). In the end, it was just a decision that has turned out to work fine for most users most of the time, even if there's probably users out there that have maybe been bitten by the issue once or twice.
So sometimes it's good to step back and look holistically at all this. When focused so closely at the feature, every potential downside can be magnified _enormously_.
@CyrusNajmabadi I'm learning a lot myself here, I think that there's many brilliant people in the community and I really think that the design team is doing an amazing job, I really appreciate that you put the time and efforts to listen to us and explain to us everything as much as possible, I hope that together we will make C# even better. ❤️
@CyrusNajmabadi
I don't get it, that same _exact_ argument could be applied just as easily to for
and foreach
loops:
// C# 6.0
int count;
foreach (var value in foo.GetValues(out count)) {
// do stuff
}
// count in scope here
// C# 7.0
foreach (var value in foo.GetValues(out var count)) {
// do stuff
}
// count not in scope here
As such I still think that it doesn't make sense to have while
differ from for
/foreach
.
I don't get it, that same exact argument could be applied just as easily to for and foreach loops
It could :).
But we felt that it was more important that the foreach-variable and out-variable be consistently scoped. Different consistencies. Different weights and all that.
As such I still think that it doesn't make sense to have while differ from for/foreach.
Heard and understood. :)
@CyrusNajmabadi
Heard and understood. :)
So, effectively, we had some meetings behind closed doors (after a notably long dark period in terms community involvement), here's what we came up with at the 23rd hour, all opinions will be promptly forwarded to /dev/null
.
I think that there are reasonable ways to increase confidence in these decisions by at least making some effort as to how the scoping rules could enable various coding styles while not hindering others. But it sounds like there's no desire whatsoever to include the community. It was too late before these notes were posted.
It's a real shame and I can't imagine it's doing much to the confidence of this process being open and inclusive.
So, effectively, we had some meetings behind closed doors (after a notably long dark period in terms community involvement), here's what we came up with at the 23rd hour, all opinions will be promptly forwarded to /dev/null.
No. The feedback was heard and understood. :)
We are discussing it. I'm not arguing with you. I'm stating that i understand your position and see the merit in it.
But it sounds like there's no desire whatsoever to include the community.
We are literally taking about it _right now_ :)
It was too late before these notes were posted.
This has not been stated by anyone else. :)
I'm really bummed that by telling you that i've heard and understood your argument, and i've been participating in this discussion in good faith, that you're stating that the feedback is going to /dev/null
. :-/
@CyrusNajmabadi
We are discussing it.
Awesome!
I'm really bummed that by telling you that i've heard and understood your argument, and i've been participating in this discussion in good faith, that you're stating that the feedback is going to
/dev/null
. :-/
Text medium sucks that way. I read it differently than you had intended it and came to the (wrong) conclusion that the above was either already considered and discarded or that it was too late to consider. Probably more the former as I assume that many of the arguments I've raised has come up before, but because of those "dark times" we weren't privy to them or the rebuttals and conclusions. I apologize for getting worked up about it. :(
All that feedback is fair. And i think, as we move forward, we should always be cognizant of ways to do things better. We've come a long way, but we are _not_ done. We can always be improving and we're going to continue working on it.
One important thing though is that these things take time, and they're likely going to take many iterations. Even as we improve, you'll likely still run into things that still irk you and which we can still do better at. I ask that you just work with us and accept that we're trying very hard, juggling a bazillion things at once, and hoping that we can "get there" one day. In other words, we think we're probably a lot like you all with all the constraints and tradeoffs you likely have to make day in an day out as well :)
I think all aspects of this has been illuminated, and we are starting to go around in circles.
I hear some consternation that this happened late in the C# 7.0 cycle. That is true. As I said above, we only came up with this compromise approach quite late. I posted these notes shortly after, and we have been discussing them since. I wish we had come up with the approach earlier, and had a longer runway for discussion, but it is what it is.
The original wave of discussion on this thread confirmed what we also experienced internally: There are pros and cons to either approach, and reasonable people can disagree on their weighting. We've explained why we landed on the side we did, and some people agree, others don't. We'll just have to agree to disagree. I genuinely appreciate the differing viewpoints; they are invaluable in the decision making progress, and add new perspectives and insights.
I love it when we can make language decisions that are obviously right, and everybody unites behind them. This is just not one of those cases. I love when all the dimensions of consistency align, but in this case they just don't. The language as it is didn't anticipate the features we're adding to it, and when we do, sometimes the seams just have to show a bit. We try to make these choices as thoughtfully, collaboratively and open-mindedly as we can. And so it is in this case also. We really, really try to make the decision that serves the developers well for a long time.
Given all of this, we still feel we've made the right decision on scopes. Thanks everyone for helping us flush out the pros and cons. We'll keep trying to think of mitigations for some of the cons; e.g. #14651. But the core scope rules are the best we can do, and we'll stick with those.
As an aside, I hope we can keep tones of outrage and scorn out of these discussions. We should always assume the best intentions and efforts on the part of people speaking up here.
Thanks again,
Mads
@Kaelum You're being unfair though, just because they don't think the same as some of us, doesn't mean they don't listen.
@MadsTorgersen
As an aside, I hope we can keep tones of outrage and scorn out of these discussions.
In my opinion this is coming from the quiet period leading up to this revelation. There was no public forum or community input. By the time this was revealed it was already too late. This is very unlike many of the other proposals that have been hashed out both here and on CodePlex. Even if you reached the exact same conclusion I think that we'd all feel a little better to have been a part of the process and to understand the thinking that ultimately led to the decision. Not everyone was happy with the end result of interpolation, but I don't think too many people can claim that their voice wasn't heard.
Many of the decisions above seem quite arbitrary. There's no reason for while
to differ in scope behavior from for
and foreach
and the argument for that case is specious at best. Then at least the language would be more consistent with itself and lend itself easier to loop refactoring. Beyond that I think minor scope tweaks in specific circumstances could make this an easier pill to swallow for those who'd prefer the functional approach.
The real shame is that it's hard to really tell the mistakes until it's way too late. String interpolation being one of them; the flexibility of it making it completely unsuitable for many situations due to the overhead. LINQ is another for largely the same reason. My fear is that the problems with these scope rules will become painfully apparent when we move from simplistic "bouncer" validation to complex recursive patterns. Maybe/hopefully a form of match
will be able to resolve those concerns, but we won't be able to know for at least another iteration.
@Kaelum Yeah, I know where you're coming from, I'm not convinced either but I don't take that as if they don't listen to us because I really think they do.
I'm not thrilled with last moment changes and such but can I do anything to change it? nop, I can only look forward and hope they will improve. :)
If they are fully convinced that this is the right path to take here and we can't convince them otherwise; meaning, they are completely confident with their choice then all I have left is to trust this decision and I do! regardless to what I think.
Many of the decisions above seem quite arbitrary. There's no reason for while to differ in scope behavior from for and foreach and the argument for that case is specious at best.
They seem specious _to you_. I put forth those arguments because they make sense to _me_ :)
Note that, as has been mentioned already, it's unlikely we're going to reach consensus on our differing opinions here. Specifically, that i view consistency between loops as _less_ important than consistency between variable-introducing concepts.
It's ok to disagree and to have different opinions. Trust me, that's the norm for everything we do :)
The real shame is that it's hard to really tell the mistakes until it's way too late. String interpolation being one of them; the flexibility of it making it completely unsuitable for many situations due to the overhead. LINQ is another for largely the same reason.
I would question if we feel like those are actually mistakes. Linq is not suitable for _some_ situations. But it's suitable in an enormous number of other situations. It was never a goal for linq (and most other features) to be suitable in _all_ situations.
Note that we discussed the possibility of this being a mistake _at great length_. We also discussed if the original proposal _itself_ would be a mistake. After all, if we started from a narrow scope, we could _never_ change that either. If it turned out the narrow scope was a mistake, then widening would itself lead to breaking changes that would be unacceptable.
We laid out all our options, and used our best judgement as to what would be acceptable. That's what we do with nearly every single 'interesting' feature we do. There's rarely the case where a feature is simply "totally right" on every count. Every feature usually has tradeoffs of some form, and this was no different.
Note: as mentioned earlier. This is going a direction i'm not particularly interested in. Namely a circular one. :) The best way to move forward is to at least understand the positions and accept what they are, even if they don't match yours.
For example, i don't view your thoughts on loop consistency to be specious. I just view them as different from mine. Similarly, there are people on the LDM (including myself) that view proposed solutions as actually worse-overall due to the negative impact on other scenarios. They're not wrong, they're just a different balance. One that we ended up deciding to go against.
If it makes you feel any better, there have been zero C# releases that shipped that i was totally happy with wrt my own balance of pros/cons with language features. Literally none :) There have always been tradeoffs that optimized for other use cases, or for other developer concerns. In the end though, i've seen it borne out that more often than not these delicate balance questions have worked out positively. I would not say that's universally true. But it's been quite healthy and we've been able to rev the language seven times over with great results and with a language i'm still super proud of and think will be great for users for a long long time :)
@CyrusNajmabadi
I put forth those arguments because they make sense to me :)
And they make perfect sense to me as well. But when the argument applies equally in two places then it should cancel itself out. Honestly I only pushed while
to see if there was actually any room for discussion. The only scope change that might've actually had an impact here would've been on ternary expressions, which I admit was much more of a stretch.
If it turned out the narrow scope was a mistake, then widening would itself lead to breaking changes that would be unacceptable.
I agree. That's why I'm so hesitant about this being released in any form in C# 7.0 after such a radical 180° shift without some _serious_* consideration. Ultimately I'm mostly annoyed by the lack of transparency on this particular issue.
I hope that it works out for the best. I view C# 7.0 as _primarily_ setting a lot of the foundation for the much larger feature set of pattern matching. In my opinion any such fundamental rules should be viewed against that greater picture, not the individual features. out var
is a "cute" feature, a minor evolution. Recursive pattern deconstruction is the end game.
With the scoping rules changed a lot of other dominos would seem to have fallen. The wider scope obviates the need for let
deconstruction. Combine that with the mutability of pattern variables and it no longer connects pattern deconstruction with readonly
locals, calling into the question the use of let
as shorthand for readonly var
. The obviation of let
further calls into the question potentially extending LINQ let
to support pattern deconstruction in a sequence. It seems that months of potential planning is either up in smoke or at least up in the air.
* Which may have happened, but behind closed doors to which the public was not privy and we seem to only get inkling of the arguments behind the decisions after pressing about them.
It has been useful having @CyrusNajmabadi and @MadsTorgersen explain the team's thinking here, for at least we now know why this train wreck was forced upon the language and that, despite the formers claims to be listening, no matter how much we shout here, it's going to happen.
So it appears that we got to where we are because:
out var
and decl ... out
to behave the same, so that one-line out ... return
guards become possible. Allegedly many other people have asked for this completely illogical feature too, though these "many other people" clearly don't frequent github and must have asked in private.out var
has to leak its declaration out into the surrounding block.for
and foreach
already have the concept of introducing vars that do not leak out. So the decision has been made that out var
in a for
or foreach
will behave consistently with the way those loops currently work, and thus inconsistently with the way out var
will work for the rest of the language. out var
is leaky, "pattern variables" introduced with is var
will be leaky as well, except with for
and foreach
where they won't leak. This means that out var
and is var
are consistently inconsistent across the language, which apparently is a good thing, for _everyone_ will prefer that they be consistently inconsistent, as opposed to inconsistently consistent.var out .. return
guards are more important than anything else and so out var
's have to be allowed to leak. So could we all shut up now please as anything else is circular argument.At least those of us who hang out on Stack Overflow can look on the bright side. Explaining this mess to confused developers is going to enable us to earn serious amounts of SO rep...
It seems to me that there should indeed be granular control of the variable scope by the user. By default the scope should be as in the old proposition until modified appropriately by the user. Since users have to understand scope anyway, why not let them choose it? I have seen Mads' response above
"It's an explicit goal not to allow granular control of the scope of a given variable. A given construct should be a scope, and all variables contained inside it are scoped by it, regardless of how they are declared."
and I can't see any downsides to that. Here is a semi-serious example of how it would look, introducing the new keyword 'leaked'.
//Continues to work
if (o is bool b) ...b...; // b is in scope
else if (o is byte b) ...b...; // fine because bool b is out of scope
...; // no b's in scope here
GetCoordinates(out var x, out var y);
...; // x and y not in scope :-(
GetCoordinates(leaked out var x, leaked out var y);
...; // x and y are in scope :-/
// Before
void M(object o)
{
if (!(o is int i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, but i is out of scope :-(
}
// After
void M(object o)
{
if (!(o is int leaked i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, and is in scope :-)
}
// Furthermore, it can be consistent with for and foreach
for(leaked int i=0; i < 10; i++) {
// Work
}
// i is in scope and 10 here.
foreach (leaked var item in collection) {
// Work
}
// item is accessible, but null.
// Muddy cases:
for (int i = foo(out int j);;);
// Neither in scope
// Option
for (leaked int i = foo(out int j);;);
// i in scope but not j?
// Another option
for (int i = foo(out int leaked j);;);
// j in scope but not i!
// Also that makes for:
if (...)
if (...)
if (o is int i) ...i...
...; // i is not in scope
if (...)
if (...)
if (o is int leaked i) ...i...
...; // i is in scope but almost certainly not definitely assigned
An another note, there could be reuse of existing keyword such as extern.
Edit: After reviewing more comments above, it seems to me that wide out var i
would have been a better solution. Yes, it's a new keyword but from the users point of view it's just what you'd expect.
While I don't object the current decision, I agree with most of HaloFour's points. And I also hope the scope for while
is reconsidered at least.
I trust your C# team and i understand you are very busy. But the published info is under expectation. I don't get the clear picture even though I read here everyday. It is also due to the complexity and big circle of effect of the features for this version. I believe you have worked out every aspects and you have a consistent design for now and future. But I cannot stop thinking: "do they missed something? is this really right for the future?" You understand the thing so well so that it will feel boring to repeat. But I don't get the clear picture, so I'm not sure.
I'd like some MSDN style education article, which summerizes the new features and what it will be like to use them together. Then we can also see broader user feedbacks.
Current design is unacceptable, and there do not exist any rational POV that validate it.
Leaking variables are counter intuitive, as there do not exist locality, and locality is one of foundation of intuition, imo. Bouncer pattern is poor reason for leakage from conditions. Most users want clarity, and will define variable before condition, than somewhere inside expression, especially if expression is long and complex. Leakage should be controlled and available only when user demand it explicitly. Then lazy users can write:
void M(object o)
{
if (!(o is outer int i)) throw new ArgumentException("Not an int", nameof(o));
...; // we know o is int, and i is intentionally leaked and in scope :-)
}
While leakage is wrong, then inability to reuse variables is just insane and counterproductive. Language designers should know, that every developer has up to ten slots, that can store random names of variables. Beyond that limit it become difficult to track variables to determine how they are useful at given point in code. This is why locality and any effort to reduce variables are always welcome, while opposite will be always rejected. Current design promotes explosion of variables used only once, and prohibit to reuse them in expression for type switch. What's more, even variables defined outside expression can't be used to type switch. Such design do not make sense at all.
Instead insane and unjustified explosion, team should promote useful implosion, and allow not to create new variables at all, but retype existing variables:
object n = 1;
if (n is int n)
{
// n here as int
}
// n here as object
This is very intuitive, as variable itself is meaningless, and we just want to see 'true face' of given object.
While I obviously agree with letting users take care of the scope leaking, I would personally find the reuse in a way similar to above quite confusing. That would be a breach of existing rule preventing variables with same name in inner scope when there is one in outer scope.
The scope should be outer (widening) by default, requiring the coder to explicitly change the scoping when they require inner scope (narrowing). Prefixing the variable identifier with ~
to specify inner scope is lightweight comprise.
Whatever is decided upon, will definitely affect other features like declaration expressions (#254) which can't be super useful compared to its statement counterpart if it doesn't restrict its own scope. In my opinion, when a language introduce a new idiom, it should make it preferable over other alternatives wherever it can; something that did not happened with string interpolation. We already have the "leaky" version of this: just declare the variable beforehand. Now, if we strive for readability and conciseness I think it's better to reconsider other mechanisms to help with that while we also take advantage of the scoping, like guard
or let
to see if they address said issues instead of accepting the current situation as permanent (because it's not). Or just postpone it until to the next version when we see the clear picture of all features that need to be consistent with each other together. However, that would make the current next version a point release.
@vbcodec,
Whilst it's being nice having hugely disparate voices united in condemnation of this badly thought through feature, I have to utterly disagree with your ideas on variable scope. The following is more wrong than @MadsTorgersen's daft leakage ideas:
object n = 1;
if (n is int n)
{
// n here as int
}
// n here as object
The above goes completely against the way scope currently works in that an inner-scoped var cannot be delared with the same name as an existing one. The same rules should apply to out var
and is var
.
@DavidArno @vbcodec To me, it looks like variable shadowing but just like existing out
arguments I think it's useful to be able to bind to existing variables also in patterns (#13400) which happen to address widening scope of the said variables into the enclosing block, though with a little more typing.
Alternative #14777
@DavidArno
This retype of variables is new concept, as currently variables have static types, regardless of context. Really there will be second hidden variable (int for this example), that will be used inside new scope, and original variable must be updated, when hidden copy was changed.. But this retyping may be helpful, as determining new non-conflicting name for variable, very often is laborious and irritating.
@alrz
Or just postpone it until to the next version when we see the clear picture of all features
This is best case if they won'f fix it (and their POV BTW). But there are other factors. New VS is upcoming, and they must enumerate some new significant features into marketing media, and binary literals or digit separators do not look like holy grail.
Team also said that they want implement features in piecemeal fashion, where feature is maturing over multiple language versions, than in one big step. This is risky strategy, as for many features beginning is determined by the end, if feature should be working right.
@vbcodec In my opinion this sort of _shadowing_ can be confusing and introduce even more bugs.
@eyalsk
As for implementation, better way is to use hidden variable defined similar to ByRef / ref.
Do you mean implementation or conceptually troubles ? Do not think there are any problems, except short time adaptation by users.
Can you provide examples how it is wrong ?
@vbcodec
What I wrote was just a general statement but I see at least three problems here:
n
is used for the same reason, thus, a different name would be more suitable.n
the object
inside the scope of the condition due to what I described in paragraph 1.@eyalsk
It's unlikely that n is used for the same reason, thus, a different name would be more suitable.
Yes, outside I want object, inside I want integer. What I want is different type, not different name. Such retyping is like function overloading or overriding, which is pretty good idea.
It's likely that you might want to access n the object
This is real issue, but easy to remove, as you can use different name for this case. Retyping is optional.
Finally, it might not be easy to follow the flow of logic, especially with multiple shadowed variables , hence, why it's going to be confusing.
This is pretty relative, and heavily depend how well and clear code is created. Multiple retyping can create mess, and multiple random names also can create mess. But retyping has two advantages:
1 You always know where is source of retyped variable. This may be improved by intellisense that show both original and current type.
@vbcodec
Yes, outside I want object, inside I want integer. What I want is different type, not different name. Such retyping is like function overloading or overriding, which is pretty good idea.
Well, if that's what you want then I'm not gonna argue about it but to me it doesn't make sense, I'd probably want to give it a more specialized name.
I don't _think_ that function overloading or overriding or even members that hides through inheritance and were redeclared has anything to do with it nor they are applicable to your argument.
This is real issue, but easy to remove, as you can use different name for this case. Retyping is optional.
So it's optional 20% of the time because after some processing in most cases I'd imagine that you may want to assign a value back to the original variable.
Multiple retyping can create mess, and multiple random names also can create mess.
So if one thing can create a mess and another already creates a mess, do you want to create a chaos? 😄
1 You always know where is source of retyped variable. This may be improved by intellisense that show both original and current type.
If you can't reach the variable inside the scope because it's being _shadowed_, why exactly do we need the intellisense to show any information about it?
- Retyping allow to update structures, because single object is always used. Without it there is need to use explicit cast, which is bit messier.
I'm not sure I understand this part, for it to be used as an int, it needs to be unboxed first so you must cast it, the name doesn't really matter.
@eyalsk
Maybe I didn't stated it clearly. Retyped variable isn't separate variable with the same name as 'external' variable and different type. It is just pointer (like those in C++), linked to 'external' variable. So, it has consequences:
Currently pointers can be defined only as input parameters defined with ByRef / ref keywords, I suggest to be created also in typeswitch expressions, if names of variables (checked and created) are identical.
@vbcodec Pointer or not that would still represent a variable that is shadowing another variable. The fact that they might "point" to the same thing doesn't matter. Also, such a proposal does nothing to address this problem in the greater picture of recursive pattern matching as there would be no original variable to shadow.
@vbcodec Your idea can't work because it would make the type system unsound as C# allows write access to the shadowed variable via variable capture:
C#
object str = "asdf";
System.Action bar = () => { str = 3; };
if (str is string) {
bar();
}
The decision should not be based around 'reducing code lines' but instead about reducing explicit variable typing while maintaining scoping sanity. For me the trade off should be having to create a condition variable when necessary to have wider scope:
var condition = s.TryParse(out var i, out var j);
if(!condition) return;
//i, j are in scope
The same logic applies to pattern matching variables: Choose consistent scoping as default, and fall back to outer declaration for wider scoping.
...Or create new keywords that make leakage an explicit choice. It is a shame that there is an assumptive justification around 'new keywords' being somehow worse than unintuitive and contradictory semantics.
@Andrew-Hanlon Because _this ship has sailed_ we have two options here drown or adapt. 😆
@eyalsk _Begrudgingly_ adapt...
For not wanting to add keywords, the new C# 7 {if}
and {while}
statements are an odd inclusion:
{if(n is int i && i > 5) return;}
:wink:
This is probably mentioned before, but looking into the future, for this specific use case, I personally prefer tuples and pattern matching over out params when it comes to returning multiple values from a method.
// TryX returns a nullable tuple
let (result1, result2) = TryX(arguments, moreArguments) else return;
// TryX returns bool
if (!TryX(arguments, moreArguments, out var result1, out var result2)) return;
let
was proposed exactly to introduce pattern variables in a broader scope without requiring an additional indention in the first place. The downside to this approuch is that this needs modifications on the declaration-site rather than just the use-site, but in my opinion it wouldn't be an issue when you actually want to refactor to use new features.
EDIT: F# special cased this and you can use this pattern without touching the method declaration.
match Int32.TryParse str with
| true, value -> // succeed
| _ -> // failed
This is the largest disaster I've ever seen when it comes to computer language design.
@Kaelum Messaged you offline about some additional questions i have. Please let me know if you get the message or not. Thanks!
Hrmm. i tried your @me.com email address. Is there a better one i can try? You can reach me at [email protected]. Thanks!
void M(object o)
{
if (!(o is int i)) i = 0;
...; // would be nice if i was in scope and definitely assigned here
}
My god this is horrid, please don't let this become best practice. What's wrong with:
void M(object o)
{
int i = o as int ?? 0;
}
What's wrong with: ...
It doesn't work for more than simple expressions. For example, any code that performs multi-statement logic.
LDM notes for Jul 15 2016 are available at https://github.com/dotnet/csharplang/blob/master/meetings/2016/LDM-2016-07-15.md
I'll close the present issue. Thanks
I do understand the arguments for this but I just wanted to point out that going against a fundamental concept for convenience is a very terrible idea and it opens the door to other really really bad implementations for the sake of convenience. This leads to very dirty and convoluted concepts that are hard to grasp and makes learning it that much harder. Further, it's not even a new feature at this point. It's simply a convenience tool. If I were to guess I'd say that it generates the exact same IL as declaring the variable before the function call. In any case, that's exactly how it behaves and it's unexpected.
@MLaukala I don't know what "fundamental concept" you're referring to.
In any case, this feature was introduced in C# 7.0. Since then we've release 7.1, 7.2, and 8.0 is about to be released. I don't think we can revisit long completed design decisions. I have no idea how you imagine pattern-matching would work without this.
Most helpful comment
I think all aspects of this has been illuminated, and we are starting to go around in circles.
I hear some consternation that this happened late in the C# 7.0 cycle. That is true. As I said above, we only came up with this compromise approach quite late. I posted these notes shortly after, and we have been discussing them since. I wish we had come up with the approach earlier, and had a longer runway for discussion, but it is what it is.
The original wave of discussion on this thread confirmed what we also experienced internally: There are pros and cons to either approach, and reasonable people can disagree on their weighting. We've explained why we landed on the side we did, and some people agree, others don't. We'll just have to agree to disagree. I genuinely appreciate the differing viewpoints; they are invaluable in the decision making progress, and add new perspectives and insights.
I love it when we can make language decisions that are obviously right, and everybody unites behind them. This is just not one of those cases. I love when all the dimensions of consistency align, but in this case they just don't. The language as it is didn't anticipate the features we're adding to it, and when we do, sometimes the seams just have to show a bit. We try to make these choices as thoughtfully, collaboratively and open-mindedly as we can. And so it is in this case also. We really, really try to make the decision that serves the developers well for a long time.
Given all of this, we still feel we've made the right decision on scopes. Thanks everyone for helping us flush out the pros and cons. We'll keep trying to think of mitigations for some of the cons; e.g. #14651. But the core scope rules are the best we can do, and we'll stick with those.
As an aside, I hope we can keep tones of outrage and scorn out of these discussions. We should always assume the best intentions and efforts on the part of people speaking up here.
Thanks again,
Mads