switch with var/null strange behavior

Given the following code:

string someString = null;
switch (someString)
{
    case string s:
        Console.WriteLine("string s");
        break;
    case var o:
        Console.WriteLine("var o");
        break;
    default:
        Console.WriteLine("default");
        break;
}

Why is the switch statement matching on case var o?

It is my understanding that case string s does not match when s == null because (effectively) (null as string) != null evaluates to false. IntelliSense on VS Code tells me that o is a string as well. Any thoughts?


Similiar to: C# 7 switch case with null checks


Solution 1:

Inside a pattern matching switch statement using a case for an explicit type is asking if the value in question is of that specific type, or a derived type. It's the exact equivalent of is

switch (someString) {
  case string s:
}
if (someString is string) 

The value null does not have a type and hence does not satisfy either of the above conditions. The static type of someString doesn't come into play in either example.

The var type though in pattern matching acts as a wild card and will match any value including null.

The default case here is dead code. The case var o will match any value, null or non-null. A non-default case always wins over a default one hence default will never be hit. If you look at the IL you'll see it's not even emitted.

At a glance it may seem odd that this compiles without any warning (definitely threw me off). But this is matching with C# behavior that goes back to 1.0. The compiler allows default cases even when it can trivially prove that it will never be hit. Consider as an example the following:

bool b = ...;
switch (b) {
  case true: ...
  case false: ...
  default: ...
}

Here default will never be hit (even for bool that have a value that isn't 1 or 0). Yet C# has allowed this since 1.0 without warning. Pattern matching is just falling in line with this behavior here.

Solution 2:

I'm putting together multiple twitter comments here - this is actually new to me, and I'm hoping that jaredpar will jump in with a more comprehensive answer, but; short version as I understand it:

case string s:

is interpreted as if(someString is string) { s = (string)someString; ... or if((s = (someString as string)) != null) { ... } - either of which involves a null test - which is failed in your case; conversely:

case var o:

where the compiler resolves o as string is simply o = (string)someString; ... - no null test, despite the fact that it looks similar on the surface, just with the compiler providing the type.

finally:

default:

here cannot be reached, because the case above catches everything. This may be a compiler bug in that it didn't emit an unreachable code warning.

I agree that this is very subtle and nuanced, and confusing. But apparently the case var o scenario has uses with null propagation (o?.Length ?? 0 etc). I agree that it is odd that this works so very differently between var o and string s, but it is what the compiler currently does.

Solution 3:

It's because case <Type> matches on the dynamic (run-time) type, not the static (compile-time) type. null doesn't have a dynamic type, so it can't match against string. var is just the fallback.

(Posting because I like short answers.)