Switch based on generic argument type
Asked Answered
H

6

21

In C# 7.1 the below is valid code:

object o = new object();
switch (o)
{
    case CustomerRequestBase c:
        //do something
        break;
}

However, I want to use the pattern switch statement in the following scenario:

public T Process<T>(object message, IMessageFormatter messageFormatter) 
    where T : class, IStandardMessageModel, new()
{
    switch (T)
    {
        case CustomerRequestBase c:
            //do something
            break;
    }
}

The IDE gives me the error "'T' is a type, which is not valid in the given context" Is there an elegant way to switch on the type of a generic parameter? I get that in my first example you are switching on the object and the second I'd want to switch on the type T. What would be the best approach to do this?

Hanshansard answered 3/12, 2018 at 16:25 Comment(7)
You probably want to make use of typeof(...).Graz
If your method only supports a fixed list of types, it shouldn't be generic in the first place. Just have overloads for the different types your method supports.Gobble
Honestly, this looks like a code smell to me. Any time you think you need to switch on a type almost certainly means that you should be refactoring the code so that isn't needed.Kinglet
@DavidG: Yes, switching on code type is a code smell. But it's one we have all done occasionally over the years - sometimes it's the best (or only) solution to a problem. With the new syntax, the smell is greatly reduced - you get a very clear syntactic rendition of the intent of the program. David Christopher Reynolds: you may want to file an issue with Microsoft. You may have found an edge case they should fix.Eunuchoidism
@Eunuchoidism I've done it before is a poor reason to suggest doing it again.Kinglet
@DavidG: OK, but there are reasons to do this: 1) the types have a common base class (or interface), but you don't control the source for the types (or the cost of the refactor would be too high). You could add an Extension method, but that method would probably include a switch on type. 2) the classes don't have a common inheritance root, and it would make little sense to add one. 3) I'm sure their are others. Sometimes, code smell is unavoidable (particularly when maintaining grotty old code).Eunuchoidism
@Eunuchoidism In all those cases simple method overloading is better. What added value does generics bring? You are essentially implementing each overload inside a switch case...Bord
C
17

Below are two different classes called Foo and Bar. You can use one instance of any of these classes as a parameter to a function named Process. After all, you can perform pattern matching as shown in the example function. There is a function named Test for the usage example..

public class Foo
{
    public string FooMsg { get; set; }
}

public class Bar
{
    public string BarMsg { get; set; }
}

public class Example
{
    public T Process<T>(T procClass) where T : class
    {
        switch (typeof(T))
        {
            case
                var cls when cls == typeof(Foo):
                {
                    var temp = (Foo)((object)procClass);
                    temp.FooMsg = "This is a Foo!";

                    break;
                }

            case
                var cls when cls == typeof(Bar):
                {
                    var temp = (Bar)((object)procClass);
                    temp.BarMsg = "This is a Bar!";

                    break;
                }
        }

        return
            procClass;
    }

    public void Test(string message)
    {
        Process(new Foo() { FooMsg = message });
        Process(new Bar() { BarMsg = message });
    }
}
Calhoun answered 3/2, 2019 at 15:23 Comment(0)
C
8

I agree that there are situation when this approach is faster and not so ugly, and also agree that in any case a better solution should be found, but sometimes the trade-off doesn't pay... so here is a solution (C# 9.0)

return typeof(T) switch
{
    Type t when t == typeof(CustomerRequestBase) => /*do something*/ ,
    _ => throw new Exception("Nothing to do")
};
Convalesce answered 28/8, 2021 at 9:2 Comment(0)
S
0

I'm going to preface by saying that in general I agree with all the commenters that say switching on a generic T is probably not a good idea. In this case I would advice him to stick with identifying the object, casting it, and then passing it to an appropriate handler.

However, I've been writing a custom binary serializer that needs to be fairly efficient and I've discovered one case where I feel the kind of switching (or if statement) he's asking for is justified, so here's how I managed it.


public T ProcessAs<T>(parameters)
{
    if (typeof(T) == typeof(your_type_here)
    {
        your_type_here tmp = process(parameters);
        return Unsafe.As<your_type_here, T>(ref tmp);
    }
    else if (typeof(T) == typeof(your_other_type))
    {
        your_other_type tmp = otherProcess(parameters);
        return Unsafe.As<your_other_type, T>(ref tmp);
    }
    else
    {
        throw new ArgumentException(appropriate_msg);
    }
}

Note that Unsafe.As<T>(T value) can be used if you're dealing with a class instead of a struct.

Stale answered 24/5, 2022 at 20:53 Comment(0)
A
0

If we ignore the codesmell discussion as per comments, an easy readable implementation (hack) can look like this:

public T Process<T>(string number)
{   
    switch (typeof(T).FullName)
    {
        case "System.Int32":
            return (dynamic) int.Parse(number);
        case "System.Double":
            return (dynamic) double.Parse(number);
        default:
            throw new ArgumentException($"{typeof(T).FullName} is not supported");
    }
}

Even if you are # with your generic constraints, this is probably bound to cause issues unless you are the sole programmer ;)

Accelerando answered 11/1, 2023 at 9:3 Comment(0)
T
0

I've used typeof(T), typeof(T) t when t == typeof(X), typeof(T).Name, and typeof(T).FullName variations (suggested in other answers), but I've never been happy with any of them. They're either too complex, or too slow.

typeof(T) when t == typeof(X) is probably the best, however, it's performance is questionable as the compiler seems to treat the when clauses as a series of if ... else if ... statements. Run the debugger and trace it to see what I mean. String variations have similar concerns - getting type names and comparing strings seems like unnecessary overhead.

What we really want is a solution that uses native switch hash lookup behavior for optimal performance.

So here's another solution that reduces complexity:

  1. Create a dummy variable T
  2. Assign an new value
  3. Use the dummy variable of type T in the switch statement

Result:

public T Process<T>(object message, IMessageFormatter messageFormatter) 
    where T : class, IStandardMessageModel, new()
{
    T dummy = Activator.CreateInstance(typeof(T));
    
    switch (dummy)
    {
        case CustomerRequestBase _:
            //do something
            break;
    }
}

T requires a constructor, which in this case is perfect as the method is qualified with where T: class, new() - being a class and having a default constructor. So we can instantiate the dummy variable, and use that dummy variable to execute standard switch variable case Type functionality.

Warning: T dummy = default won't work as default is usually Null, and we can't use Null in the switch statement. This is why Activator.CreateInstance() was used.

Discard (_) in the case statement is used to discourage use of the dummy variable. Presumably the case statement will have other logic for handling 'message'.

Tortoiseshell answered 25/3, 2023 at 4:24 Comment(0)
R
-3

I was able to get this working by creating a temporary variable of T and then switching case on that. Its a bit of a silly workaround about I think it has the fewest compromises

public void TypeSwitchMethod<T>()
{
    T typeHolder = default(T)!;
    switch(typeHolder)
    {
        case Foo:
        //DO STUFF
        break;
        case Bar;
        //Do OTHER STUFF
    }
}
Reverence answered 13/12, 2023 at 0:2 Comment(1)
This does not work for complex types where the default is null. See the discussion here: #42951333Eloquence

© 2022 - 2024 — McMap. All rights reserved.