Roslyn: C# 7 Out variables inconstant scoping with flow control qualifiers

Created on 9 Mar 2017  路  26Comments  路  Source: dotnet/roslyn

Version Used:
C# 7 (VS 2017)

Steps to Reproduce:
if (Foo(out int i))
{
i.ToString();// valid scope
}

i.ToString();// 'i' can be used outside of the 'if' scope block?

Expected Behavior:
'i' should only live in the if's scope.

Just like any flow control qualifier such as 'for' or 'while' the scope of variables should start within the '()' brackets. Otherwise this is just syntax sugar without any real value as its inconstant with the rest of the C# syntax. Maybe I'm missing something but just feels wrong.

Actual Behavior:
'i' can be used outside of the if's scope.

Area-External Area-Language Design Language-C#

All 26 comments

Attention @gafter, this is exactly what I have concluded as well. The email I sent to you was from a similar example as this.

The expected behavior is a compilation error.

This was a design decision because the pattern

if (!Foo(out int i))
{
  // some sort of fail-fast mechanism (throw, return, continue, etc)
  return;
}
// use i here 

was expected to be common

was expected to be common

Note: our investigations into this showed that it was common. In Roslyn, for example, >60% of all cases were ones where the scope was needed to live past the conditional check.

Otherwise this is just syntax sugar without any real value

There is a lot of real value here :)

Perfect, thank you so much for the clarification.

@CyrusNajmabadi The correct logical "flow control" consistent with the C# syntax layout is the use:

int i;
if (!Foo(out i))
{
// some sort of fail-fast mechanism (throw, return, continue, etc)
return;
}
// use i here

The scope of 'i' is now unambiguous in your example as it breaks consistency with all other flow control types. That method effectively makes it impossible to do stuff like:

if (!Foo(out int i))
{
// some sort of fail-fast mechanism (throw, return, continue, etc)
return;
}

if (!Foo2(out float i))
{
// some sort of fail-fast mechanism (throw, return, continue, etc)
return;
}

Which is exactly what I do all the time with stuff like:

for (int i = 0; i != 123; ++i)
{
// do stuff...
}

for (int i = 456; i != 0; --i)
{
// do stuff...
}

The goal should be to remove the need for stuff like the example below in 'if' blocks:

// block 1 (FYI: would be nice if 'block' was a keyword)
{
if (!Foo(out int i))
{
// some sort of fail-fast mechanism (throw, return, continue, etc)
return;
}
}

// block 2
{
if (!Foo2(out float i))
{
// some sort of fail-fast mechanism (throw, return, continue, etc)
return;
}
}

Its a false equivalence to assert "classic out method" = "new out method" in terms of C# syntax flow control as they simply don't match other syntactic patterns. Scoping is the real need/want here (as it solves an actual problem). That new syntax sugar doesn't simplify the code if that was the goal. Line condensing isn't a valid argument for simplification if only to ignore that it complexifies the syntactic inconsistencies overall thus increasing the learning curve of the lang.

Its like creating a game where all ball objects fall down with the green color material and fall up with the red color material. The new syntax is like having a ball fall down with the color red. It breaks the rules of the game.

Sorry if this sounds harsh. I've been talking about it with others and we seem to agree that the syntax doesn't solve any real world problems and in the long run only serves to complexify the C# lang without a significant advantage for the added complexification.

That method effectively makes it impossible to do stuff like:

Yes. But the workaround is to use different names. Each approach has pros/cons. If we chose narrow scope the pros was you could reuse names, but the cons was you couldn't use the feature if you wanted wide scoe.

If we chose wide scope (which we did) the pro was you could use it for wide/narrow scenarios, but the con was you'd need different names. We decided we wanted a feature that was applicable to hte most scenarios possible and so we went with the final design.

I've been talking about it with others and we seem to agree that the syntax doesn't solve any real world problems

The language design team discussed this extensively and decided that it did solve enough real problems without presenting issues that were problematic enough to outweigh them. In the end we felt it was a good feature to ship and that we were happy with the tradeoffs we chose.

C# is now starting to remind me of bug prone javaScript arguments in terms of syntax scoping. As C# is my favorite lang, I'm saddened by this kind of thinking.

Example of bad scoping ideologies: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

My voice probably doesn't carry any weight here so not the end of the world as its optional but not something I can ever see myself practically taking advantage of as visually constant scoping rules and syntax are very important to me. I would rather write it out in a way that better illustrates the intent of the code.

Anyway...

I would rather write it out in a way that better illustrates the intent of the code.

This option is always available to you. Out-vars, though, were introduced to enable many more scenarios, and to provide much terser code for what turns out to be very common coding patterns. You can definitely choose to not use it for those purposes if you have no need for it.

--

Note: there is nothing stopping someone from using out-var for narrow scoping purposes. You can absolutely do that. The only restriction is that you need to provide unique names if the names would clash. IMO, that's likely a good thing anyways as that way there is far less confusion in the code as to what any particular identifier means.

Out-vars, though, were introduced to enable many more scenarios

What situations? They function the same as the classic method correct? This is simply a keyword placement offset minus ';' from my understanding.

provide much terser code

Yes the idea is terser in the sense of elegant / fluent if used to solve scoping issues that other langs don't. However simply shortening the height of code to lengthen the width isn't really any more or less 'terser'.
An actual compression of code has always exited in the form of doing stuff like "int a, b, c;" which isn't possible in the new 'terser' concept. Thus the argument for variable name context through scoping is a more valid argument in my mind.

The only restriction is that you need to provide unique names if the names would clash.
IMO, that's likely a good thing anyways as that way there is far less confusion in the code as to what any particular identifier means.

Because scope determines variable context and is used to free up common English names for re-use. Less confusing is being miss-represented here as scoping rules are now in a state of dissonance. I just don't see any objective arguments for this scoping move.

Just my 2 cents.

What situations? They function the same as the classic method correct? This is simply a keyword placement offset minus ';' from my understanding.

Not necessarily. For example, with most original 'out' scenarios, you could not use var. that's because it was not legal to write:

```c#
var values;
something.TryDoSomethingElse(out values);

So you'd have to write one of:

```c#
Dictionary<string, ImmutableArray<CodeAction>> values;
something.TryDoSomethingElse(out values);

// or

var values = (Dictionary<string, ImmutableArray<CodeAction>>) null;
isomething.TryDoSomethingElse(out values);

The former means lots of extra types that you have to write, even if you really prefer 'var' and don't want to have to provide them. The latter means the value is now definitely assigned at the declaration point, even if you don't want that, and want to get definite assignment errors if you do something wrong (say if there was branching going on).

The new feature allows you to simply trim that down to:

c# something.TryDoSomethingElse(out var values);

The code can now use 'var' (in line with the rest of your code if you like that style). It doesn't need extra statements. And it ensures there's no assignability issues.

Thus the argument for context and the ability to control that context through scoping and variable placement is a more valid argument in my mind.

We disagreed. As i already mentioned, we wanted a more inclusive feature that would make things more broadly applicable, while not overly restricting the sort of patterns you want to write. The only downside of the approach we took was that you would need to write unique names for variables. But we viewed that as a reasonable restriction. After all, if you do have code like this, it's likely that it would be valuable for hte names to be unique to help keep the code clear.

I just don't see any objective arguments for this scoping move.

All language design is subjective :)

It's a weighing of different opinions and different pros/cons. We clearly didn't weigh them as you did. And so the feature is viewed as a net negative for you. That's ok. Indeed, that's how all language features go. We've never added a single feature that did not end up being viewed negatively by some number of our audience**. The goal isn't to please everyone or to try to only pick things that somehow are 'objectively universally positive'. The goal is to put in changes that we think provide enough overall bang/buck for users. We end up being the final arbiters of this and we'll invariably never convince everyone that our decisions were correct.

--

* I've been working on the language for a very long time, and we continually deliver features that *I personally do not like :)

The new feature allows you to simply trim that down to:
"something.TryDoSomethingElse(out var values);"

I 100% agree with this being much more 'terser' in that context. Just not the scoping aspect and I don't think this is entirely subjective either.

If I use your example you can do stuff like:

something.TryDoSomethingElse(out var values);
values.Add(someValue);//valid

But the scoping should still apply when I do:

if (something.TryDoSomethingElse(out var values) == true)
{
values.Add(someValue);// valid
}

values.Add(someValue);// invalid

As you can still be terse with doing:

var result = something.TryDoSomethingElse(out var values)
if (result == true)
{
values.Add(someValue);// valid
}

values.Add(someValue);// valid

Yes. We considered those options and ended up deciding that we felt the final place we landed on was still best for all the factors we were weighing. Your disagreement is understood and is certainly not isolated. We just simply felt differently than you here :)

Well I don't "feel" my argument is better I "know" it is (cough cough).
Guess I'm just going to have to folk the lang (cough) and call it 'C# Perfect+'.

Kk I'm done ;)

You're welcome to discuss this further here if you like, but FYI we have moved language design discussions to https://github.com/dotnet/csharplang

@gafter k tnx for info

@CyrusNajmabadi I told you this would happen. And that was only 2 days after the release. What do you think will happen over the next few months?

@Kaelum As i mentioned in my above post, we are cognizant of the pros/cons here. Any way we went we would end up with people asking questions and potentially being surprised that the feature did not match their intution. After all, that's one of the reasons we changed things in the first place :)

In the end we settled on a design that we felt enabled the most scenarios while introducing the least intrusive restrictions. Other approachs would have had a different balance of tradeoffs, and that would have certainly impacted some set of customers.

And that was only 2 days after the release.

So, pretty much like every release :D

@CyrusNajmabadi Customers probably aren't always the best language design engineers though. Many times "customers" are doing things wrong. It doesn't make sense to design things wrong to match a customers bad practice. If this is the thought process being used the lang isn't going to go down a good path in the long run. I'm saying that just in general, not to argue this specific topic.

Many times "customers" are doing things wrong.

We didn't think customers are doing wrong things here. The use case seemed sensible and felt appropriate for the language.

If this is the thought process being used the lang isn't going to go down a good path in the long run.

This is the thought process that we use for all language features :) It's how we got through all previous versions so far.

We didn't think customers are doing wrong things here.

I would be interested to know what other lang group ideologies are on this, especially Scala. I don't think lang design groups for Scala, C++ or Nim etc would make this choice. Java might but Java makes a lot of bad choices that I wouldn't emulate. Just a thought. (I only care because I like C#, not to harp).

We certainly look at what other languages are doing. But our philosophies are our own. :)

I don't think lang design groups for Scala, C++ or Nim etc would make this choice

That's fine. We're not trying to be those languages. Nor do we share all the same beliefs as those languages as to what balance to strike with features. We routinely diverge greatly from these in many ways, and sometimes we overlap in some ways. We definitely look outward. But we're always working to bring in ideas in ways that we feel best fit what we want from this language.

@Kaelum

I apologize for the confusion. I'm _not_ an employee of Microsoft and my opinions are my own. I actually misread Neal's comment when he closed this issue. I initially read it as "we should continue the conversation on a new thread" in the https://github.com/dotnet/csharplang. I reread that he stated the conversation could continue here.

Again, I'm sorry for the confusion and by all means continue the conversation.

I continued the topic here: CS Git Page Link

Was this page helpful?
0 / 5 - 0 ratings