Many Questions: switch on type


I hope everyone had a good fourth of July weekend. I certainly did. I spent the weekend hiking around the Olympic peninsula with my girlfriend and capped it off watching the fireworks here in Seattle. On to our question of the week:


We get a lot of requests for addditions to the C# language and today I’m going to talk about one of the more common ones – switch on type. Switch on type looks like a pretty useful and straightforward feature: Add a switch-like construct which switches on the type of the expression, rather than the value. This might look something like this:


switch typeof(e) {
case int:    … break;
case string: … break;
case double: … break;
default:     … break;
}


This kind of statement would be extremely useful for adding virtual method like dispatch over a disjoint type hierarchy, or over a type hierarchy containing types that you don’t own. Seeing an example like this, you could easily conclude that the feature would be straightforward and useful. It might even get you thinking “Why don’t those #*&%$ lazy C# language designers just make my life easier and add this simple,  timesaving language feature?”


Unfortunately, like many ‘simple’ language features, type switch is not as simple as it first appears. The troubles start when you look at a more significant, and no less important, example like this:


class C {}
interface I {}
class D : C, I {}

switch typeof(e) {
case C: … break;
case I: … break;
default: … break;
}


Here the set of cases overlap. When ‘e’ is of type ‘D‘ both ‘case I‘ and ‘case C‘ are applicable. Now what? As language designers, we need to precisely specify what happens in cases like these. The choices to deal with this kind of situation are:





  • process the case tests in textual order


  • make definition of potentially overlapping cases a compile time error


Processing the cases in textual order would make the above switch semantically different from this:


switch typeof(e) {
case I: … break;
case C: … break;
default
: … break;
}


This runs completely counter to people’s intuition about switch. Programmers would be extremely surprised to learn that reordering the case labels had an affect on which case was chosen.


Better yet, what about this:


switch typeof(e) {
default: … break;
case
I: … break;
case C: … break;
}


Would the default case always be chosen? Ouch!


Alternatively, making definition of potentially overlapping cases a compile error would not yield a useful feature. It would make switching on interfaces an error for virtually every useful case, and would also make switching on an extensible type hierarchy – like System.Windows.Forms.Control –  also difficult if not impossible.


So the conclusion is that we thought pretty deeply about this feature, but couldn’t find a way to add switch on type in a way which is both intuitive and broadly useful. This situation is not uncommon in language design. The C# language design team has discarded many potential features for similar reasons.


This example ilustrates what is the most important lesson I’ve learned from Anders Hejlsberg while I’ve been on the C# language design team. Often the result of good design is to cut the feature.


Peter
C# Guy


Comments (15)

  1. marcod says:

    "Often the result of good design is to cut the feature"

    This is so true, I learned that first at

    The Design and Evolution of C++

    http://www.research.att.com/~bs/dne.html

    Very good job C# language design team !!!

  2. AndrewSeven says:

    Eek 😉

    When you check to see if something "is a", it can be many different things.

    Should it really be approximate or should it be an exact type match?

    I usualy look at it in a different way (compiler doesn’t like this):

    switch(something.GetType())

    {

    case typeof(typeA):…

    case typeof(typeB):…

    }

    This can be changed to all strings that contain the typeNames, but it become ugly to manage.

    The switch on type problem comes up from time to time, what is the best way to "can it" with the current C#?

  3. Luke says:

    Presumably switching on a type would be based on an exact match and not consider inheritance, and so be much like switching on a string. But I can see how that might be confusing too, and explicit "if" statements were never really all that bad to begin with. Yep, sometimes less is more. Now about "yield"….

  4. Attention – this is important: Good Design

    Peter Hallam wrote a very important entry on his WebLog:Many…

  5. If people have control over the classes in question, they can add a virtual method or property, which is the "correct" OO way of doing it. If you have no control over the classes, you can still do a switch with the name of the type.

  6. Attention – this is important: Good Design

    Peter Hallam wrote a very important entry on his WebLog:Many…

  7. Luke Stebbing says:

    You definitely don’t want to switch on the exact type and ignore the inheritance hierarchy, since that breaks LSP (Liskov Substitution Principle), which is a BIG NO-NO (when is-a stops meaning is-a, type contracts get violated).

    This is actually a subtle instance of the diamond inheritance problem. ‘switch’ can be thought of as locally defining anonymous methods for each class listed in the statement (each ‘case’ is one of these definitions). But D inherits the *implementations* of the anonymous methods from both C and I, so we encounter the multiple implementation inheritance quagmire by another name.

  8. Garry Trinder says:

    Luke: Are you kidding about ignoring inheritance ? If you do so – you are trashing out idea of OOP.

    If you are inheriting from some class you must expect to inherit almost all functionality of it and not be forced to reimplement it.

    Regarding feature cutting – it’s good to cut them only in case if there are good workarounds exists.

  9. James Hancock says:

    What I would like to see is this:

    switch(tabs.SelectedTab) {

    case pnlGeneral:

    break;

    }

    Why? because it’s a pain in the ass to type this:

    switch(tabs.SelectedTab.Name) {

    case "pnlGeneral":

    break;

    }

    And get "pnlGeneral" exactly right and you don’t get a compile error if you change the name of the control etc.

    instance specific switching would be great. I don’t really care about type switching, I can’t see that as good no matter how you cut it, but switching based on object, that would be great and highly useful and there’s no problem iwth what it does with confusion or anything. "If the selected tab is this control…" Is obvious.

  10. Joe Duffy says:

    I’d personally find it quite intuitive to process in lexical order. In my mind,

    switch (typeof(e)) {

    case I: … break;

    case C: … break;

    default: … break;

    }

    would expand to

    if (typeof(I).IsInstanceOf(e)) { … }

    else if (typeof(C).IsInstanceOf(e)) { …}

    else { … }

    (or the equivalent IL using the isinst instruction)

    Similar to how the following gets resolved:

    try {

    throw e;

    }

    catch (I i) { … }

    catch (C c) { … }

    catch { … }

    If e is always dynamically typed as a D, well the second catch block is dead (assuming no other exception can get thrown). You won’t know this, but since D implements I, I don’t see how the behavior would be confusing…?

    The compiler might choose to warn when it statically knows you’re writing nonsensical overlapping switches. But then again: given that I can write obj x = e; if (x is I) { … } else if (x is C) { … } else { … }, I suppose the feature wouldn’t save me too much (any?) code.

  11. Well, while C# extended the C-idea of switch (to include strings),

    however as you noted ranges of switch cases are disjoint (except for

    the default case).

    However ML languages since the very beginning included a notion

    of pattern matching. For example one can say something like (Nemerle

    notion):

    match ((x, y)) {

    | (1, 2) => "one two"

    | (1, _) => "one something else"

    | _ => "anything else"

    }

    So you can match over quite complicated structures. In ML this is

    limited to records, tuples and algebraic datatypes. In Nemerle as

    a natural .NET extensions, we have included ability to matching

    on properties on runtime types.

    Anyway this is quite powerful mechanism, especially useful when

    working with tree-like data structures.

    However the specification of all ML variants I’ve used states that

    patterns can overlap (like in the example above) and the matching

    is done top-bottom. This is quite natural, as most people probably

    think of switch/match as a series of if-s (this is why default: goes

    at the end, isn’t it?). The only problem is when a pattern can *never*

    be taken — compiler issues a warning then.

  12. Well, while C# extended the C-idea of switch (to include strings),

    however as you noted ranges of switch cases are disjoint (except for

    the default case).

    However ML languages since the very beginning included a notion

    of pattern matching. For example one can say something like (Nemerle

    notion):

    match ((x, y)) {

    | (1, 2) => "one two"

    | (1, _) => "one something else"

    | _ => "anything else"

    }

    So you can match over quite complicated structures. In ML this is

    limited to records, tuples and algebraic datatypes. In Nemerle as

    a natural .NET extensions, we have included ability to matching

    on properties on runtime types.

    Anyway this is quite powerful mechanism, especially useful when

    working with tree-like data structures.

    However the specification of all ML variants I’ve used states that

    patterns can overlap (like in the example above) and the matching

    is done top-bottom. This is quite natural, as most people probably

    think of switch/match as a series of if-s (this is why default: goes

    at the end, isn’t it?). The only problem is when a pattern can *never*

    be taken — compiler issues a warning then.

  13. barrkel says:

    Something to be aware of: C# already has a switch on type. It’s called ‘catch (<type>)’, and it works in strict order, with well defined semantics for subtypes and supertypes, and for the default clause.