106

In C# 7+, can I switch directly on a System.Type?

When I try:

    switch (Type)
    {
      case typeof(int):
        break;
    }

it tells me that typeof(int) needs to be a constant expression.

Is there some syntatic sugar that allows me to avoid case nameof(int): and directly compare the types for equality? nameof(T) in a case statement is not completely good because namespaces. So although name collision is probably not be applicable for int, it will be applicable for other comparisons.

In other words, I'm trying to be more type-safe than this:

    switch (Type.Name)
    {
      case nameof(Int32):
      case nameof(Decimal):
        this.value = Math.Max(Math.Min(0, Maximum), Minimum); // enforce minimum 
        break;
    }
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
toddmo
  • 20,682
  • 14
  • 97
  • 107
  • 6
    @Servy, no I don't. My example isn't the answer. It's the counter-example. – toddmo Mar 28 '17 at 21:47
  • 5
    Rather than using a switch, can you build a `Dictionary` that calls the function you want for a given type? Your code would be cleaner that way, I think. – xxbbcc Mar 28 '17 at 21:48
  • 6
    @toddmo `it tells me that typeof(int) needs to be a constant expression.` That's literally the answer to your question. – Servy Mar 28 '17 at 21:48
  • @xxbbcc, I've seen that approach. I don't think that's cleaner. It seems a little convoluted. I'm looking for what's built into the switch statement, which so far isn't all that well documented. – toddmo Mar 28 '17 at 21:49
  • 3
    It's described here: https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/ Switch statements with pattern – Matthias247 Mar 28 '17 at 21:49
  • 1
    @toddmo I'm just curious, why do you think it's convoluted? I personally find it very flexible and clean to read but maybe I'm missing something. – xxbbcc Mar 28 '17 at 21:50
  • @Matthias247, that doesn't give an example that works directly on `System.Type`. The example given only works on subclasses like with polymorphism. – toddmo Mar 28 '17 at 21:50
  • @toddmo When comparing two possible implementations, my personal preference has been to go with the solution that actually works over the one that either doesn't compile or doesn't work properly. By all means choose the "cleaner" solution when comparing two solutions that compile and actually produce the correct output. – Servy Mar 28 '17 at 21:50
  • 3
    @toddmo Maybe you should explain why you want to switch on `System.Type`, when you can just switch on the value? I guess you would need `typeof(value)` before. – Matthias247 Mar 28 '17 at 21:53
  • Can you expand on your code a little bit and tell us more of what the function is supposed to do? Are you just checking Int32 and decimal? – Magnus Mar 28 '17 at 21:53
  • 1
    @Matthias247, in this block I don't have a value; `this.value` hasn't been set. So I can't switch on `this.value`. – toddmo Mar 28 '17 at 21:55
  • @Magnus, if the Type is int or decimal, I want to enforce the minimum and maximum on the value. If you need more context than that let me know. – toddmo Mar 28 '17 at 21:59
  • See @Servy, I was right. There's some sugar that's not in the example on switch in the "what's new in c# 7" article that's more direct than the name. – toddmo Mar 28 '17 at 22:00
  • Why not use an `if`? – Magnus Mar 28 '17 at 22:11
  • @Magnus, Just personal preference I suppose. in actuality, there may be more cases. `if` results in more sets of brackets being used and the use of `else if`, which, in my mind, becomes not a single selector, but a series of unrelated tests (that's how my mind compiles an `if` block). After all, that's why `switch` was created, to simplify a selector based on a single criteria. `if` blocks can introduce any arbitrary criteria; so when reading the code you have to look harder to ensure you understand what it's really doing. – toddmo Mar 28 '17 at 22:19

10 Answers10

129

The (already linked) new pattern matching feature allows this.

Ordinarily, you'd switch on a value:

switch (this.value) {
  case int intValue:
    this.value = Math.Max(Math.Min(intValue, Maximum), Minimum);
    break;
  case decimal decimalValue:
    this.value = Math.Max(Math.Min(decimalValue, Maximum), Minimum);
    break;
}

But you can use it to switch on a type, if all you have is a type:

switch (type) {
  case Type intType when intType == typeof(int):
  case Type decimalType when decimalType == typeof(decimal):
    this.value = Math.Max(Math.Min(this.value, Maximum), Minimum);
    break;
}

Note that this is not what the feature is intended for, it becomes less readable than a traditional if...else if...else if...else chain, and the traditional chain is what it compiles to anyway. I do not recommend using pattern matching like this.

  • 1
    Can you chain `when`s? Instead of `case Type t when (t == typeof(int) || t == typeof(decimal)) :` – toddmo Mar 28 '17 at 22:04
  • 1
    @toddmo (Responding to your edited comment) With `||` it's not what chaining would be, but any condition is valid, including one composed of multiple subexpressions combined using `&&`, `||`, and any other operator you like. –  Mar 28 '17 at 22:07
  • 3
    Why do you declare fresh `Type` variable instead of using discard pattern: `case var _ when type == typeof(int)`? – user4003407 Mar 29 '17 at 03:01
  • @PetSerAl In the general case: to prevent the switch value from being evaluated multiple times. In this case: I don't know if it would be better, but it probably at least wouldn't be worse than what I put in my answer. I still wouldn't use either though. –  Mar 29 '17 at 06:07
55

The issue raised here by the OP is that you can't use the new C# 7 type-based switch feature when you don't have an actual instance of the switched-upon type available, and you instead have only its putative System.Type. The accepted answer, summarized as follows, works well for exact type matching (minor improvement shown here, but see my final example below for yet further streamlining)...

Type type = ...
switch (type)
{
    case Type _ when type == typeof(Int32):
    case Type _ when type == typeof(Decimal):
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum);
        break;
}

The important point to note is that for derived reference type hierarchies, this will not exhibit the same behavior as an if... else chain which uses the is keyword for matching. Consider:

class TBase { }
class TDerived1 : TBase { }
class TDerived2 : TBase { }
sealed class TDerived3 : TDerived2 { }

TBase inst = ...

if (inst is TDerived1)
{
    // Handles case TDerived1
}
else if (inst is TDerived2)
{
    // Handles cases TDerived2 and TDerived3
}
else if (inst is TDerived3)
{
    // IMPOSSIBLE (never executed)               <---  !
}

Since TDerived3 "is-a" TDerived2, when using is matching, both cases are handled by the second branch. (And since TDerived3 is marked "sealed" in this example, no instance can ever match the third branch.)

This highlights the difference at runtime between 'strict' or 'exact' type equality versus the more nuanced notion of type subsumption (type "compatibility"). Because the types in the OP's question were ValueType primitives (which can't be derived-from), the difference couldn't matter. But if we adapt the 'exact type matching' of the accepted answer with the example classes shown above, we will get a different result:

Type type = ...

switch (type)
{
    case Type _ when type == typeof(TDerived1):
        // Handles case TDerived1
        break;

    case Type _ when type == typeof(TDerived2):
        // Handles case TDerived2
        break;

    case Type _ when type == typeof(TDerived3):
        // Handles case TDerived3                 <---  !
        break;
}

In fact, C# 7 won't even compile a switch statement which corresponds to the if / else sequence shown earlier. (n.b. It seems like the compiler should detect this as a warning, rather than an error, since the harmless result is just a branch of inaccessible code--a condition which the compiler deems a warning elsewhere--and also considering that the compiler doesn't even detect, at all, the seemingly identical situation in the if / else version). Here's that:

enter image description here

In any case, which one of the alternate behaviors is appropriate, or if it even matters, will depend on your application, so my point here is just to draw attention to the distinction. If you determine that you need the more savvy type-compatibility version of the switch approach, here is how to do it:

Type type = ...

switch (type)
{
    case Type _ when typeof(TDerived1).IsAssignableFrom(type):
        // Handles case TDerived1
        break;

    case Type _ when typeof(TDerived2).IsAssignableFrom(type):
        // Handles cases TDerived2 and TDerived3
        break;

    case Type _ when typeof(TDerived3).IsAssignableFrom(type):
        // IMPOSSIBLE (never executed)            <---  !
        break;
}

As before, the third case is extraneous, since TDerived3 is sealed for the current example.

Finally, as I mentioned in another answer on this page, you can simplify this usage of the switch statement even further. Since we're only using the when clause functionality, and since we presumably still have the original switched-upon Type instance available in a variable, there's no need to mention that variable in the switch statement, nor repeat its Type (Type, in this case) in each case. Just do the following instead:

Type type = ...

switch (true)
{
    case true when typeof(TDerived1).IsAssignableFrom(type):
        break;

    case true when typeof(TDerived2).IsAssignableFrom(type):
        break;

    case true when typeof(TDerived3).IsAssignableFrom(type):
        break;
}

Notice the switch(true) and case(true). I recommend this simpler technique whenever you are relying only on the when clause (that is, beyond just the situation of switching on System.Type as discussed here).

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
  • I think you should swap the 2 things like this. It should be: typeof(TDerivedN).IsAssignableFrom(type). And also put case #3 first. – N73k May 28 '19 at 20:12
  • @N73k The order that I specified for `IsAssignableFrom` is correct. It is an instance method call on the ***lesser-derived*** `Type`. As you note, swapping the order is directly related to the order of the case statements, which therefore is also correct as specified. – Glenn Slayden May 31 '19 at 19:14
  • Hmm. I don't think so. For example if type is a TDerived4 (where TDerived4 : TDerived3) then you'd want your last line (the one with "TDerived3") to be triggered. But no case statements get triggered because TDerived4 is not assignable from any of the TDeriveds in the case statements. – N73k Jun 03 '19 at 19:32
  • @N73k Mea culpa. I updated the answer to incorporate your correction. – Glenn Slayden Sep 21 '19 at 09:46
22

Starting with Paulustrious's idea of switching on a constant, but striving for the most readability:

  Type type = GetMyType();
  switch (true)
  {
    case bool _ when type == typeof(int):
      break;
    case bool _ when type == typeof(double):
      break;
    case bool _ when type == typeof(string):
      break;
    default:
      break;
  }

What's readable is subjective. I used to do something similar in VB a long time ago so I got used to this form (but in VB the bool _ was not needed so it wasn't there). Unfortunately in c# the bool _ required. I'm using c# 7.0 and I think switching on a constant may not be supported in earlier compilers but I am not sure about that, so try it if you want to. I think it's kindof amusing that the S/O code formatter doesn't know about when yet.

You wouldn't want to do this of course if you need the case variable, like for subclasses.

But for arbitrary boolean expressions it is more suited, for example:

  switch (true)
  {
    case bool _ when extruder.Temperature < 200:
      HeatUpExtruder();
      break;
    case bool _ when bed.Temperature < 60:
      HeatUpBed();
      break;
    case bool _ when bed.Y < 0 || bed.Y > 300:
      HomeYAxis();
      break;
    default:
      StartPrintJob();
      break;
  }

Some will argue this is worse than if..else. The only thing I can say is switch forces one path and it's impossible to break the switch statement itself, but it is possible to leave out an else and break an if..else into multiple statements unintentionally, possibly executing two "branches" accidentally.

Switching on a Type is really just an arbitrary switch because what we are really switching on is a property of the variable. Unless and until we can do case typeof(int) (case on something that is not a constant expression), we are stuck with something akin to this if we don't want to use string constants, which in the case of type names, are not in the framework.

toddmo
  • 20,682
  • 14
  • 97
  • 107
  • 2
    You pretty much covered it there. It is purely another way of if-then-else-if. But if we adopt that concept what is the purpose of any switch statement? It improves readability, less likely to forget a 'not' and as you say it catches the missing `else`. FYI. c# V7 is the first version to support this, and the first version where the order of the case statements makes any difference. I understand about your VB. I used Powerbuilder and that had a `choose` statement which gave you similar capabilities. Finally can have multiple 'ifs' in a when clause. – Paulustrious Oct 11 '17 at 20:38
  • 1
    I had this same issue using my own types, this implementation really helped clean up what was a messy list of if-else-if-else statements. – Daniel Aug 11 '20 at 09:57
14

Although the question is on C# 7 but for those with a C# 8 version or higher, you can use the newer syntax which is shorter and I think is nicer as well.

type switch
{
    Type _ when type == typeof(int) || type == typeof(decimalType) =>
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum),
    _ => // default case
};

Another elegant option:

Type.GetTypeCode(type) switch
{
    TypeCode.Int32 or TypeCode.Decimal =>
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum),
    _ => // default case
};

For more info: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression

Misha Zaslavsky
  • 8,414
  • 11
  • 70
  • 116
4

@toddmo suggested the following:

switch (true)
{
    case bool _ when extruder.Temperature < 200:
        HeatUpExtruder();
        break;

    // etc..
    default:
        StartPrintJob();
        break;
}

...but why not go even further in his pursuit of simplicity. The following works just as well without needing the bool type qualification, nor the extraneous _ dummy variable:

switch (true)
{
    case true when extruder.Temperature < 200:
        HeatUpExtruder();
        break;

    // etc.
    default:
        StartPrintJob();
        break;
}
Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
0

Adding to the above answer, if you do not need the value to be used inside the case statement, you can use _. This syntax can be used to remove an unused variable warning message.

switch (this.value) {
  case int _:
    //Do something else without value.
    break;
  case decimal decimalValue:
    this.value = Math.Max(Math.Min(decimalValue, Maximum), Minimum);
    break;
}
Karthikeyan VK
  • 5,310
  • 3
  • 37
  • 50
0

I know it's not applicable in all cases, but maybe my example will help someone. In ASP.NET Core I implemented my custom model binder provider and I had to resolve binder type depending on model type.

The initial idea (apart from the if/else block ofc but I kept thinking it could be shorter) was the switch:

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        switch (context.Metadata.ModelType)
        {
            case Type _ when context.Metadata.ModelType == typeof(Model1):
                return new BinderTypeModelBinder(typeof(Binder1));
            case Type _ when context.Metadata.ModelType == typeof(Model2):
                return new BinderTypeModelBinder(typeof(Binder2));
            case Type _ when context.Metadata.ModelType == typeof(Model3):
                return new BinderTypeModelBinder(typeof(Binder3));
        }

        return null;
    }

This is the same with a dictionary:

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var bindersByType = new Dictionary<Type, Type>
        {
            {typeof(Model1),  typeof(Binder1)},
            {typeof(Model2),  typeof(Binder2)},
            {typeof(Model3),  typeof(Binder3)}
        };

        return bindersByType.TryGetValue(context.Metadata.ModelType, out Type binderType) ? new BinderTypeModelBinder(binderType) : null;
    }

Credits for the idea go to @xxbbcc who posted this in a comment to the first question

kurdemol94
  • 358
  • 5
  • 12
-1

I found a simple and efficient way. It requires C# V7 to run. The switch("") means all cases will be satisfied up to the when clause. It uses the when clause to look at the type.

List<Object> parameters = new List<object>(); // needed for new Action
parameters = new List<object>
{
    new Action(()=>parameters.Count.ToString()),
    (double) 3.14159,
    (int) 42,
    "M-String theory",
    new System.Text.StringBuilder("This is a stringBuilder"),
    null,
};
string parmStrings = string.Empty;
int index = -1;
foreach (object param in parameters)
{
    index++;
    Type type = param?.GetType() ?? typeof(ArgumentNullException);
    switch ("")
    {
        case string anyName when type == typeof(Action):
            parmStrings = $"{parmStrings} {(param as Action).ToString()} ";
            break;
        case string egStringBuilder when type == typeof(System.Text.StringBuilder):
            parmStrings = $"{parmStrings} {(param as System.Text.StringBuilder)},";
            break;
        case string egInt when type == typeof(int):
            parmStrings = $"{parmStrings} {param.ToString()},";
            break;
        case string egDouble when type == typeof(double):
            parmStrings = $"{parmStrings} {param.ToString()},";
            break;
        case string egString when type == typeof(string):
            parmStrings = $"{parmStrings} {param},";
            break;
        case string egNull when type == typeof(ArgumentNullException):
            parmStrings  = $"{parmStrings} parameter[{index}] is null";
            break;
        default: throw new System.InvalidOperationException();
    };
} 

This leaves parmStrings containing...

System.Action 3.14159, 42, M-String theory, This is a stringBuilder, parameter[5] is null

Paulustrious
  • 609
  • 1
  • 11
  • 17
  • 2
    You are switching on a string and whenning on a Type, instead of switching on a Type, is that faster? because it's a bit harder to follow. The person looking at it is scratching their head for a moment wondering what the case expressions mean (turns out they don't mean anything to the compiler, but the variable names make it seem as they do, as you are using them like comments, but this is not readily clear). – toddmo Oct 07 '17 at 16:30
  • @toddmo You're right. I have looked at the code generated and it is simple. Far less expensive than some of the earlier solutiosn. I didn't consider that the variable names would cause confusion. But once you have seen it and understand, it's far easier next time you come across it. I use the String variable names as comments. Say there are four bool or bool? variables. I will call once case TTFN. (N=null). The next, say TxxT is conditions 1 & 4 true and 2 & 3 are irrelevant. It makes it easy to find and of course you have the default to catch the ones you forgot. – Paulustrious Oct 08 '17 at 22:46
  • You should add the code generated. That would be pretty interesting. Anyway, the true issue is deeper than confusing maintenance developers, and I'm not smart enough ATM to explain it. It has to do with doing things that the compiler won't enforce. I guess it would be more of an issue if the whenning variable wasn't `System.Type`, but some base type with many subclasses, like `System.Exception`, for example. At any rate, the "comment variables" can get out of sync with the whenning variable. And if you think they never would, you haven't met my co-workers lol – toddmo Oct 09 '17 at 16:50
  • @toddmo. The only reason I used type in my whenning ways is that's what the OP requested. In another answer I will grab some of my real code. This however doesn't address the OP's question so it may get removed.. – Paulustrious Oct 10 '17 at 18:02
  • See my answer where I tried to eliminate the disadvantage of the confusing variable names and confusing switch expression. – toddmo Oct 11 '17 at 15:46
-1

Well, it probably break your code convention, but I use this approach;

var result = this.Value is int intValue
? Math.Max(Math.Min(intValue, Maximum), Minimum)
: this.Value is decimal decimalValue
? Math.Max(Math.Min(decimalValue, Maximum), Minimum)
: this.Value is double doubleValue
? Math.Max(Math.Min(decimalValue, Maximum), Minimum)
: throw new Exception($"Cannot handle '{this.Value.GetType().Name}' value.");

You can use Type.IsAssignableFrom() in condition statement when it is more appropriate.

Gongdo Gong
  • 1,000
  • 7
  • 18
-4

Here is an alternative which won't compile as the classes are missing:

bool? runOnUI = queuedAction.RunOnUI;  // N=null, T=true F=False
bool isOnUI = Statics.CoreDispatcher.HasThreadAccess;
bool isFF = queuedAction.IsFireAndForget;   // just makes it easier to read the switch statement
if (false == queuedAction.IsFireAndForget)
    IsOtherTaskRunning = true;

/* In the case statements below the string name is something like noFF_TN
 * The compiler ignores the string name. I have used them here to represent
 * the logic in the case statement:   ( FF = FireAnd Forget, T=true, F=false, N = null, * means 'whatever')
 * 
 *      isFF_** = FireAndForget QueuedAction
 *      noFF_** = Not FireAndForget so Wait for Task to Finish (as opposed to Complete)
 *      ****_T* = We are already on the UI thread 
 *      ****_F* = We are not on the UI thread 
 *      ****_*T = Run on the UI thread.
 *      ****_*F = Do not run on UI thread
 *      ****_*N = Don't care so run on current thread
 *      
 *  The last character is a "bool?" representing RunOnUI. It has
 *  three values each of which has a different meaning */

bool isTask;
switch ("ignore")
{
    /* Run it as an Action (not Task) on current Thread   */
    case string noFF_TT when !isFF && isOnUI && runOnUI == true:
    case string isFF_TN when isFF && isOnUI && !runOnUI == null:
    case string isFF_FN when isFF && !isOnUI && runOnUI == null:
    case string isFF_TT when isFF && isOnUI && runOnUI == true:
    case string isFF_FF when isFF && !isOnUI && runOnUI == false:
        isTask = false;
        queuedAction.ActionQA(queuedAction); // run as an action, not as a Task
        break;

    /* Create a Task, Start it */

    case string isFF_TF when isFF && isOnUI == true && runOnUI == false:
    case string noFF_TN when !isFF && isOnUI == true && runOnUI == null:     // <== not sure if I should run it on UI instead
    case string noFF_TF when !isFF && isOnUI && runOnUI == false:
    case string noFF_FN when !isFF && !isOnUI && runOnUI == null:
    case string noFF_FF when !isFF && !isOnUI && runOnUI == false:
        var cancellationTokenSource = new CancellationTokenSource();
queuedAction.Canceller?.SetCancellationTokenSource(cancellationTokenSource);
        isTask = true;
        new Task
            (
                (action) => queuedAction.ActionQA(queuedAction),
                queuedAction,
                cancellationTokenSource.Token
            )
            .Start();
        break;

    /* Run on UI and don't wait */

    case string isFF_FT when isFF && !isOnUI && runOnUI == true:
        isTask = true;
        Statics.RunOnUI(() => queuedAction.ActionQA(queuedAction), asTaskAlways: true);
        break;

    /* Run on the UI as a Task and Wait */

    case string noFF_FT when !isFF && !isOnUI && runOnUI == true:
        isTask = true;
        Statics.RunOnUI(() => queuedAction.ActionQA(queuedAction), asTaskAlways: true);
        break;

    default:
        throw new LogicException("unknown case");
}
Paulustrious
  • 609
  • 1
  • 11
  • 17