92

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

Servy
  • 202,030
  • 26
  • 332
  • 449
budi
  • 6,351
  • 10
  • 55
  • 80
  • 9
    Confirmed. I love this question, ***especially*** with the observation that `o` is `string` (confirmed with generics - i.e. `Foo(o)` where `Foo(T template) => typeof(T).Name`) - it is a very interesting case where `string x` behaves differently than `var x` even when `x` is typed (by the compiler) as `string` – Marc Gravell Jun 13 '17 at 21:48
  • 7
    The default case is dead code. Believe we should be issuing a warning there. Checking. – JaredPar Jun 13 '17 at 22:06
  • @JaredPar I get no warnings locally. But to me it is odd that `case string foo` is a miss yet `case var foo` is a hit when `foo` is resolved as `string`. That's ... very subtle – Marc Gravell Jun 13 '17 at 22:07
  • Sorry comment was confusing. Agree there is no warning. Checking to see if indeed we should be issuing one here. – JaredPar Jun 13 '17 at 22:08
  • It is odd `var` behavior should be different than `string`. – leppie Jun 13 '17 at 22:09
  • 13
    It is odd to me that the C# designers decided to allow `var` in this context at all. That sure seems like the kind of thing I'd find in C++, not in a language purported to lead the programmer "into the pit of success". Here, `var` is both ambiguous and useless, things that C# design typically seems to strive to avoid. – Peter Duniho Jun 13 '17 at 22:50
  • 1
    @PeterDuniho I wouldn't say useless; the inbound expression to the `switch` could be unpronounceable - anonymous types, etc; and it isn't *ambiguous* - the compiler clearly knows the type; it is just confusing (to me at least) that the `null` rules are so different! – Marc Gravell Jun 13 '17 at 22:58
  • @Marc: okay, I'll go along with the anonymous type scenario. But I still find the behavior confusing, non-unintuitive, and not at all C#-like. I mean, this is the language where, while it's true we had to live with problem-prone behavior with captured `foreach` variables, they _did_ eventually fix that. Inconsistency in treatment of `null` values here seems like a similar pitfall that would normally have been avoided. – Peter Duniho Jun 13 '17 at 23:03
  • 1
    @PeterDuniho fun fact - we once looked up the definite assignment formal rules from the C# 1.2 specification, and the illustrative expansion code had the variable declaration *inside* the block (where it is now); it only moved *outside* in 2.0, then back inside again when the capture problem was obvious. – Marc Gravell Jun 13 '17 at 23:08

3 Answers3

69

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.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 4
    The real issue though is that the compiler "shows" `var` to be of type `string` when it's really not (honestly not sure what the type should be admittedly) – shmuelie Jun 13 '17 at 22:38
  • @shmuelie the type of `var` in the example is calculated to be `string`. – JaredPar Jun 13 '17 at 22:40
  • in the example though `var` would match an `Int32` just as well, no? – shmuelie Jun 13 '17 at 22:44
  • _"the type of var in the example is calculated to be string"_ -- then it's not really a wildcard, is it? Your answer seems very "hand-wavy" to me, with a bit of contortion to try to force an explanation when the behavior itself is logically inconsistent. (As I noted above, I'm surprised you allow `var` at all in this context...it doesn't seem useful, or at least not enough to justify the potential for confusion and non-intuitive behavior.) – Peter Duniho Jun 13 '17 at 22:53
  • @shmuelie not given the inbound expression to the `switch` – Marc Gravell Jun 13 '17 at 22:57
  • After staring at this for a while I think I get it: `var` is of type `string` because var type resolution checks the type of `someString`. However, the rules of pattern matching result in the resolved type of `var` being moot because the var case has no other parts to the pattern (as soon as you add a `when` to the var case it isn't a wild anymore). – shmuelie Jun 13 '17 at 22:59
  • @PeterDuniho not sure how stating the facts of the feature design is considered hand wavy. – JaredPar Jun 13 '17 at 22:59
  • 5
    @JaredPar thanks for the insights here; personally I would support more emitting of warnings even when it didn't do so previously, but I understand the language team's constraints. Have you ever considered a "whinge about everything mode" (possibly on by default), vs "legacy stoic mode" (elective)? maybe `csc /stiffUpperLip` – Marc Gravell Jun 13 '17 at 23:01
  • "Hand-wavy" in the sense that it doesn't seem to address the thinking _behind_ the design. I see `var` as a "wildcard" only the sense that it matches compile-time type of an expression. I don't see any good reason that it should treat `null` differently. All your answer does is confirm the behavior was intended. It doesn't really explain _why_ that behavior is intended, nor how it benefits programmers. – Peter Duniho Jun 13 '17 at 23:05
  • After studying the output of the compiler for a while I go this: `var` cases don't check the type, only doing any "when" checks. For type safety in those checks and inside the case the type is resolved using "var type resolution". So `var x when...` is effectively saying I only care about the when side. So if there is no `when` then follows that it becomes the default. There is logic here, just needs to be documented! – shmuelie Jun 13 '17 at 23:20
  • 3
    @MarcGravell we have a feature called warning waves that's meant to make it easier, less compat breaky, to introduce new warnings. Essentially every compiler release is a new wave and you can opt into the warnings via /wave:1, /wave:2, /wave:all. – JaredPar Jun 13 '17 at 23:28
  • @JaredPar (goes to look how to opt in to that in "dotnet build") – Marc Gravell Jun 13 '17 at 23:40
  • @MarcGravell Also, you could write a Roslyn analyzer and fix for this relatively simply. –  Jun 14 '17 at 01:35
  • This gist demonstrates that `null` is typeless: https://gist.github.com/jcdickinson/e3a884eb4ee3e23f5ab7f825766ebb92 – Jonathan Dickinson Jun 14 '17 at 08:37
  • 4
    @JonathanDickinson I don't think that shows what you think it shows. That just shows a `null` is a valid `string` reference, and any `string` reference (including `null`) can be implicitly cast (reference-preserving) to an `object` reference, and any `object` reference that is `null` can be successfully upcast (explicit) to any other type, still being `null`. Not really the same thing in terms of the compiler type system. – Marc Gravell Jun 14 '17 at 08:51
  • @JaredPar you say that default will never be hit even for a bool value that isn't 1 or 0, but I've demonstrated otherwise: https://github.com/dotnet/csharplang/issues/544#issuecomment-300179855 – jnm2 Jun 14 '17 at 21:47
22

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
14

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

user541686
  • 205,094
  • 128
  • 528
  • 886