16
votes

In the Roslyn Pattern Matching spec it states that:

The scope of a pattern variable is as follows:

If the pattern appears in the condition of an if statement, its scope is the condition and controlled statement of the if statement, but not its else clause.

However the latest Microsoft "What's new" posts and presentations are showing this example:

public void PrintStars(object o)
{
    if (o is null) return;     // constant pattern "null"
    if (!(o is int i)) return; // type pattern "int i"
    WriteLine(new string('*', i));
}

Which shows the pattern match i variable used outside the if level scope of the pattern match.

Is this an oversight, or has the scoping been changed from the spec?

2
That would be the controlled statement of the if statement - juharr
@juharr, wouldn't the controlled statement only be return? - Andrew Hanlon
Yeah, I missed that. I actually think this might be referring to an i at a higher scope, but it's hard to tell since it's a code snippet for beta functionality. I guess you could get the preview and test it out. - juharr
From the documentation you have linked - the variables introduced by a pattern – are similar to the out variables described earlier, in that they can be declared in the middle of an expression, and can be used within the nearest surrounding scope. This would suggest that this works similarly to the new functionality to declare a variable in an out. From what I can see, this example is almost identical to a tryparse in a way - Alfie Goodacre
@AlfieGoodacre I saw that line in the what's new, but it seems to contradict the spec document. - Andrew Hanlon

2 Answers

8
votes

From that same documentation:

the variables introduced by a pattern – are similar to the out variables described earlier

So actually this code:

if (!(o is int i)) return; // type pattern "int i"

Is more or less equal to:

int i;
if (!(SomeParsingOn(o, out i))) return; // type pattern "int i"

That means that i is declared on the same level as the if, which means it is in scope not only for the if, but also for following statements. That this is true can be seens when you copy the if:

if (!(o is int i)) return; // type pattern "int i"
if (!(o is int i)) return; // type pattern "int i"

Gives error CS0128: A local variable named 'i' is already defined in this scope.

8
votes

I posted a similar question to the Roslyn issues and was given the answer by DavidArno:

It's long, but you can read all the gory details of why the language design team chose to "enhance" the language in this way at #12939.

TL;DR you aren't alone in thinking the change unintuitive and contradictory to the way scope have worked before. The team sadly don't care though and the change is here to stay.

It seems the decision was made that this scoping would apply, so the spec is now out of date, and this scoping is sadly here to stay:

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

Conclusion

While a little subtle, we will adopt option 3. It strikes a good balance:

It enables key scenarios, including out vars for non-Try methods, as well as patterns and out vars in bouncer if-statements. It doesn't lead to egregious and counter-intuitive multi-level "spilling". 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.