Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to do an unsafe covariant invocation of a generic method?

Tags:

c#

generics

I've got a number of classes designed to handle objects of specific types.

e.g.,

class FooHandler : Handler<Foo> {
   void ProcessMessage(Foo foo);
}

where the handler interface might be defined something like this:

interface Handler<T> {
   void ProcessMessage(T obj);
}

Now, I'd like to be able to use a dictionary of these handlers:

Dictionary<Type, Handler> handlers;

void ProcessMessage(object message) {
   var handler = handlers[message.GetType()];
   handler.ProcessMessage(handler);
}

However, C# doesn't appear to allow me to use the Handler interface without specifying a type. C# also doesn't allow me to declare interface Handler<out T> so I can't use Handler<object> in the handlers declaration.

Even this doesn't work:

Dictionary<Type, object> handlers;

void ProcessMessage(object message) {
   dynamic handler = handlers[message.GetType()];
   handler.ProcessMessage(message);
}

This seems to be solvable using reflection:

handler.GetType().GetMethod("ProcessMessage").Invoke(handler, new object[] { message });

And, of course, I could remove the generics from the Handler interface. However, the reason I went down this path is that I wanted to make the API for handlers as simple as possible. I wanted classes to specify the messages they receive and for them to be able to process those messages without having to cast the parameters in every method.

I'd prefer to avoid reflection if possible and avoiding generics altogether doesn't quite seem satisfactory.

Am I missing something obvious or am I pushing up against the limits of C#'s generics?

I realize that C# isn't Java (with Java's type erasure, this would be easy) and perhaps this might have been better solved in a more C#-like way... so I'm also interested in other approaches.

Thanks!

like image 906
Ben Avatar asked Jul 27 '11 06:07

Ben


3 Answers

There is a better way. Just embed the cast in a lambda and store the action rather than the handler:

    Dictionary<Type, Action<object>> handlers;

    void AddHandler<T>( Handler<T> handler )
    {
        handlers.Add(typeof(T), m => handler.ProcessMessage((T)m));
    }

    void ProcessMessage(object message)
    {
        var handler = handlers[message.GetType()];
        handler(message);
    }

OK, this goes beyond the scope of the question a bit, but the discussion in the comments led us here:

interface IMessage {}

class Foo : IMessage {}

interface Handler<T> where T : IMessage
{
    void ProcessMessage(T obj);
}

class FooHandler : Handler<Foo>
{
    public void ProcessMessage(Foo foo) {}
}

class Program
{
    static readonly Dictionary<Type, Action<object>> handlers = new Dictionary<Type, Action<object>>();

    static void AddHandler<T>(Handler<T> handler) where T : IMessage
    {
        handlers.Add(typeof(T), m => handler.ProcessMessage((T)m));
    }

    static void ProcessMessage(object message)
    {
        var handler = handlers[message.GetType()];
        handler(message);
    }

    public static IEnumerable<Type> GetAllTypes()
    {
        return AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes());
    }

    public static IEnumerable<Type> GetDerivedFrom<T>()
    {
        return GetAllTypes().Where(t => IsDerivedFrom(t, typeof(T)));
    }

    static bool IsDerivedFrom(Type t, Type parent)
    {
        return parent.IsAssignableFrom(t) && t!=parent;
    }

    static void Main()
    {
        var handlerTypes =
            from handlerBaseType in GetDerivedFrom<IMessage>().Select(t => typeof(Handler<>).MakeGenericType(t))
            select GetAllTypes().FirstOrDefault(t => IsDerivedFrom(t, handlerBaseType))
            into handlerType
            where handlerType!=null
            select Activator.CreateInstance(handlerType);

        foreach (object handler in handlerTypes)
        {
            AddHandler((dynamic)handler);
            Console.WriteLine("Registered {0}.", handler.GetType());
        }
    }
}

No strings involved... Of course, if you want to establish the convention by naming you could make the scanning easier and simply look up the handler type from the name of the message type as done in your comment. You could also replace the Activator.CreateInstance with an IOC container.

like image 120
jdasilva Avatar answered Oct 31 '22 03:10

jdasilva


I suspect that dynamic typing will work if you take it one step further:

void ProcessMessage(dynamic message) {
   dynamic handler = handlers[message.GetType()];
   handler.ProcessMessage(message);
}

Note how I've made message dynamic, so that the overload resolution will be applied using the actual type of the object, rather than with object.

Basically what you're trying to do isn't statically type-safe, which is why the C# compiler isn't letting you get away with it. This is fairly typical in situations where you only have a Type value at execution time.

One other option is to have a generic method yourself and execute that either with reflection or dynamic:

void ProcessMessage(dynamic message)
{
    // This will call the generic method with the right
    // type for T inferred from the actual type of the object
    // message refers to.
    ProcessMessageImpl(message);
}

void ProcessMessageImpl<T>(T message)
{
    Handler<T> handler = (Handler<T>) handlers[typeof(T)];
    handler.ProcessMessage(message);
}
like image 29
Jon Skeet Avatar answered Oct 31 '22 04:10

Jon Skeet


You have your solution in using reflection :)

That is the only way I am aware of to do this. I am using this exact approach in my ESB.


EDIT

There is a caveat in using the other approaches (I may be wrong so please comment if I'm off track). It is fine when you are registering a 'singleton' handler with the known message type up-front. Using a generic <T> means you know the type. However, in my scenario I use transient handlers depending on the message type that arrives. So I create a new handler.

This means that everything has to be done on-the-fly thus requiring reflection (except maybe using dynamic in .net 4 --- I'm on .net 3.5).

Ideas?

like image 22
Eben Roux Avatar answered Oct 31 '22 05:10

Eben Roux