5

Consider the following code:

    switch ("")
    {
        case "":
            using var s = new MemoryStream();

            break;
    }

The above code won't compile with this error: "A using variable cannot be used directly within a switch section (consider using braces)".

The fix is already in the suggestion but my question is why the following code is legit but the above isn't? Why can't C# just allow the previous code?

    switch ("")
    {
        case "":
            {
                using var s = new MemoryStream();
            }
            
            // break can be inside or outside the braces
            break;
    }
Luke Vo
  • 17,859
  • 21
  • 105
  • 181
  • Hint: The early origins of `switch` in C as a thin-wrapper over a `goto` without any scoping, e.g. Duff's Device. – Dai Dec 16 '20 at 02:03
  • I have seen similar explanation in the [related question in C++](https://stackoverflow.com/questions/92396/why-cant-variables-be-declared-in-a-switch-statement?rq=1) though it does not make sense to me because you can actually declare new variable in C# switch statement. – Luke Vo Dec 16 '20 at 02:07
  • 1
    Also, this is explained in the original proposal for C# 8.0's new `using` statement: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/using – Dai Dec 16 '20 at 02:07
  • @Dai ah I didn't know that one yet. Sure that is a good answer directly from language developers. Please quote it and make it an answer and I will mark. Thanks. – Luke Vo Dec 16 '20 at 02:09
  • Actually, I think I'm wrong - I think it's more to do with the behind-the-scenes logic that the C# compiler inserts whenever you use a `switch` statement with a non-integer value, such as a `String` - it actually creates a hidden `Dictionary` when you do that, and I think that might have something to do with it. – Dai Dec 16 '20 at 02:10
  • I am pretty sure C# `switch` for strings is compiled into `if` statements in IL. I have seen it before, they even used Hash Code for binary search to improve performance as well. – Luke Vo Dec 16 '20 at 02:12
  • 2
    Yeah, I'm not a fan of that behaviour. I always *expected* the `switch` statement in C# to have `O(1)` time complexity independent of the number of cases - and it's neat that the `switch(String)` statement is also able to do `O(1)` by using a hidden Dictionary, but I'm disappointed the new C# 7+ "switch with pattern-matching" is effectively just a piss-poor alternative syntax to a bunch of `if` statements with `O(n)` time complexity. I wish they'd update the spec to require `O(1)` performance, especially as "pattern matching" implies that the compiler will optimize it like in Haskell. – Dai Dec 16 '20 at 02:15

2 Answers2

4

C# 8.0's language proposal for the new using statement gives this explanation:

A using declaration is illegal directly inside a case label due to complications around its actual lifetime. One potential solution is to simply give it the same lifetime as an out var in the same location. It was deemed the extra complexity to the feature implementation and the ease of the work around (just add a block to the case label) didn't justify taking this route.


So as an example, consider that this...

switch( foo )
{
case 1: // Yeah, I'm in the tiny minority who believe `case` statements belong in the same column as the `switch` keyword.
case 2:
    using FileStream fs1 = new FileStream( "foo.dat" );
    goto case 4;

case 3:
    using FileStream fs3 = new FileStream( "bar.dat" );
    goto case 1;

case 4:
    using FileStream fs4 = new FileStream( "baz.dat" );
    if( GetRandomNumber() < 0.5 ) goto case 1;
    else break;
}

...is equivalent to this pseudocode (ignoring the sequential if logic):

if( foo == 1 || foo == 2 ) goto case_1;
else if( foo == 3 ) goto case_3;
else if( foo == 4 ) goto case_4;
else goto after;

{
case_1:
    using FileStream fs1 = new FileStream( "foo.dat" );
    goto case_4;

case_3:
    using FileStream fs3 = new FileStream( "bar.dat" );
    goto case_1;

case_4:
    using FileStream fs4 = new FileStream( "baz.dat" );
    if( GetRandomNumber() < 0.5 ) goto case_1;
    else goto after;
}
after:

...which the spec says "has the same effect as declaring the variable in a using statement at the same location.", so if I understand the spec correctly, the above code would be the same as this:

if( foo == 1 || foo == 2 ) goto case_1;
else if( foo == 3 ) goto case_3;
else if( foo == 4 ) goto case_4;
else goto after;

{
case_1:
    using( FileStream fs1 = new FileStream( "foo.dat" ) )
    {
        goto case_4;

case_3:
        using( FileStream fs3 = new FileStream( "bar.dat" ) )
        {
            goto case_1;
        }
        
case_4:
        using( FileStream fs4 = new FileStream( "baz.dat" ) )
        {
            if( GetRandomNumber() < 0.5 ) goto case_1;
            else goto after;
        }
    }
}
after:

I think the problem is:

  • While the jump from case_4 to case_1 is well-defined to cause fs4's disposal...
  • ...it's unclear...
    • Whether fs1 should be disposed immediately when control encounters goto case_4 (after case_1:)
    • Whether fs3 should be initialized at all when jumping from case_1: to case_4: as it would be in scope (disregarding the fact it isn't used).
    • Whether fs1 should be initialized when in case 3 and case 4, even though it would strictly-speaking be in scope.

Because the linked specification proposal only shows a backwards goto to a point before a using block (thus that using statement's subject would be out-of-scope), whereas in this case it is not well-defined if fs1 and fs3 is still in-scope (or not) when jumping forward.

Remember that using; says the object should be disposed when it falls-out-of-scope, not that it should be disposed when it's last used in the scope it's declared (which would prohibit passing it on to another method that's still using it).

There's at least two arguments for what could/should happen:

  1. Dispose fs1 as soon as it jumps to case_3, even though fs1 is still technically in-scope (if you subscribe to the "all cases share the same scope" school of thought).

    • This also ignores the main point of a using; statement which strictly binds the lifetime of its subject to the enclosing scope.
  2. Dispose fs1 only when control leaves the entire switch block (even though arguably fs1 is out-of-scope prior to that.

As the proposal mentions, it's something that could have been hammered out but probably not something that people would have agreed upon given the time constraints the language design team is under.

Dai
  • 141,631
  • 28
  • 261
  • 374
-2

You need to limit the scope of your variable

switch ("")
{
    case "":
        using(var s = new MemoryStream())
        {
           // SCOPE of var S

        }
        
        // break can be inside or outside the braces
        break;
}
Eduardo
  • 78
  • 3