On 8/31/2020 9:56 PM, Dan Smith wrote:

A competent programmer will definitely need to be able to answer the question 
"should I add 'sealed' to this switch?" I'll take a stab at enumerating all the 
cases:

- A switch statement with a 'default' case: doesn't matter. The language 
already supports 'default' in both forms (existing switch statements are 
non-sealed, existing switch expressions are sealed). Pick a favorite style. 
(Alternatively: a switch statement with a 'default' clause is implicitly 
sealed. Then it's a style question of whether or not to be explicit about it.)

- A switch statement that initializes a local variable or returns at the end of 
a method: doesn't matter. If you don't say 'sealed', flow analysis will catch 
any mistakes. (But there are some gotchas, so maybe 'sealed' (or switch 
expression) is the way to go if you don't want to think about it.)

Yeah, while in many cases flow analysis will save us, I'm not sure this is the message we want to send; it is not uncommon when presented with a flow error ("variable x might not be initialized) to "fix" it with "int x = 0".  (Gotcha, stupid compiler.)  I think these cases are prime examples of "falling out of this switch silently is a mistake", so I would advocate for sealing in all these cases, rather than hoping they used flow analysis correctly.

- A switch statement that side-effects on just a few of the possible inputs 
("possible" per static types): must use a non-sealed switch.

Not "must", since you can have a default that does nothing in a sealed switch.

But, there is a subtle difference between

    switch (x) {
        case FOO: ...
    }

and

    sealed switch (x) {
        case FOO: ....
        default: // nothing
    }

which is, what happens on remainder.  In the former, it is just another ignored non-matching input; in the latter, we throw.

I believe this is the subtle difference that will get people.

- A switch statement that is optimistically total over an enum/sealed class: 
use a sealed switch to ensure totality checking in the future. Or, if the 
totality is accidental (I cover the cases right now, but don't expect to in the 
future), use a non-sealed switch.
What we were calling optimistic totality (now, totality with remainder) is not just about enums and sealed classes, though. Consider:

    switch (foo) {
        case Foo(var x): ...
    }

There is a remainder, null, and a sealed switch will NPE on it.  (As will a pattern assignment.)

- A switch statement with a last 'case' intended to be total: use a sealed 
switch to avoid mistakes and (if it's a risk) defend against input type changes

I think that covers it? There will be some coding style preferences to work 
out, but I think this story will be intuitive to most programmers.


Yep.  I think the key difficult think here, which took us a while to see, is that the remainder shape can get complicated, especially when you get to nested deconstruction patterns over sealed types. We agreed that this is unavoidable, and hopefully will pop into the user's consciousness rarely.

The key difference between a sealed switch and a non-sealed one is that in a sealed switch, there is _no_ input value which is silently ignored.


Reply via email to