2

I'm not sure why this doesn't work. It doesn't like TResponse for the out and handlerMap add, even though TResponse is an IResponse? I figure I must be misunderstanding something about generics, or perhaps more likely, about C#. Why doesn't this work, and is there a better way to accomplish what I'm trying to do here?

private static Dictionary<Type, List<IResponseHandler<IResponse>>> _handlerMap;

public static void AddResponseHandler<TResponse>(IResponseHandler<TResponse> handler) where TResponse : IResponse
{
    List<IResponseHandler<TResponse>> handlers;
    _handlerMap.TryGetValue(typeof (TResponse), out handlers);

    if (handlers == null)
    {
        handlers = new List<IResponseHandler<TResponse>>();
        _handlerMap.Add(typeof (TResponse), handlers);
    }

    handlers.Add(handler);
}

public interface IResponseHandler<TResponse> where TResponse : IResponse
{
    void Handle(TResponse response);
}

I am getting these errors during compilation:

Error 1 The best overloaded method match for 'System.Collections.Generic.Dictionary>>.TryGe‌​tValue(System.Type, out System.Collections.Generic.List>)' has some invalid arguments C:...\NetworkManager.cs 39 13 Assembly‌​-CSharp-vs

Error 2 Argument 2: cannot convert from 'out System.Collections.Generic.List>' to 'out System.Collections.Generic.List>' C:...\NetworkManager.cs 39 61 Assembly-CSharp-vs

If I change TResponse to IResponse within the method, everything above
handlers.Add(handler) compiles fine. I don't understand why I can't add a handler of 
<TResponse : IResponse> to a List<IResponseHandler<IReponse>>?
Charlie Mulic
  • 199
  • 2
  • 12
  • _handlerMap is a dictionary of types mapping to a list of IResponseHandlers, my method says where TResponse : IResponse, I was thinking that this constraint would satisfy any type issues? – Charlie Mulic Mar 30 '13 at 20:59
  • 2
    Show us the definition of `IResponseHandler`. You seem to be assuming that if `TResponse` is a `IResponse` then `IResponseHandler` is a `IResponseHandler` which is not true in the general case. It would be true, if the `IResponseHandler` interface is covariant. – Mike Zboray Mar 30 '13 at 21:03

3 Answers3

1

The variance in C# doesn't allow you to assign an IResponseHandler<IResponse> to an IResponseHandler<T> even if there's where clause on T.

I can't tell what you're trying to do because you haven't provided all the code that's in use here; but, this will compile:

public class SomeClass<TResponse> where TResponse : IResponse
{
    private static Dictionary<Type, List<IResponseHandler<TResponse>>> _handlerMap;

    public static void AddResponseHandler(IResponseHandler<TResponse> handler) 
    {
        List<IResponseHandler<TResponse>> handlers;
        _handlerMap.TryGetValue(typeof(TResponse), out handlers);

        if (handlers == null)
        {
            handlers = new List<IResponseHandler<TResponse>>();
            _handlerMap.Add(typeof(TResponse), handlers);
        }

        handlers.Add(handler);
    }       
}

This moves the generic from the method to the class so you can define a compatible _handlerMap.

Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
1

As others mentioned - there is no way to do it `the way you're doing it'...

a) You need contravariance - for the Add to work

b) You need covariance to be able to upcastfrom IResponseHandler<TResponse> to IResponseHandler<IResponse>

(also you have another compilation problem with returning out into differnt type of List which cannot work either way)...

For a solution - you could trick it into working sort of - if this contract satisfies what you need. It's more of a 'practice example' as you lose some of the support - but depends on what you need...

interface IResponse { }
interface IResponseHandler<out TResponse>
    where TResponse : class, IResponse
{
    // add 'read-only' (simplified) properties only - that support 'covariance' - meaning no 'input parameters' of T etc.
    // void Handle(TResponse response);
}
abstract class ResponseHandler<TResponse> : IResponseHandler<TResponse> 
    where TResponse : class, IResponse
{
    public abstract void Handle(TResponse response);
}
class TestHandler
{
    private static Dictionary<Type, List<IResponseHandler<IResponse>>> _handlerMap = new Dictionary<Type,List<IResponseHandler<IResponse>>>();
    public static void AddResponseHandler<TResponse>(IResponseHandler<TResponse> handler) where TResponse : class, IResponse
    {
        List<IResponseHandler<IResponse>> handlers;
        _handlerMap.TryGetValue(typeof(TResponse), out handlers);
        if (handlers == null)
        {
            handlers = new List<IResponseHandler<IResponse>>();
            _handlerMap.Add(typeof(TResponse), handlers);
        }
        IResponseHandler<IResponse> myhandler = handler;
        handlers.Add(myhandler);
    }
    public static void Handle<TResponse>(TResponse response) where TResponse : class, IResponse
    {
        List<IResponseHandler<IResponse>> handlers;
        _handlerMap.TryGetValue(typeof(TResponse), out handlers);
        if (handlers == null) return;
        foreach (var handler in handlers)
        {
            (handler as ResponseHandler<TResponse>).Handle(response);
        }
    }
}
// and implementation...
class FirstResponse : IResponse { }
class AutomatedResponse : IResponse { }
class FirstHandler : ResponseHandler<FirstResponse>
{
    public override void Handle(FirstResponse response) { }
}
class AutomatedHandler : ResponseHandler<AutomatedResponse>
{
    public override void Handle(AutomatedResponse response) { }
}
// ...and a test...
var firsthandler = new FirstHandler();
var secondhandler = new AutomatedHandler();
TestHandler.AddResponseHandler(firsthandler);
TestHandler.AddResponseHandler(secondhandler);

var first = new FirstResponse();
var second = new AutomatedResponse();
TestHandler.Handle(first);
TestHandler.Handle(second);

There are couple things of interest, fast...

1) You need out on the base interface - to make it covariant

2) You need to keep it covariant - by not adding anything in it like Add (see the comment). Basically (and overly simplified) you need to maintain it read only (mark that this isn't true - just easier to think that way). Also that goes for all the types/other params etc. that participate in it. The compiler will guide you w/ errors

3) Pull out all the functionality from the IResponseHandler into a ResponseHandler class - that server all - there you can add your Add etc. - and override for specific cases

4) You'd need to cast to get to the 'handler' that can actually 'handle' - that (handler as ResponseHandler<TResponse>).Handle(response);

Note

...that this is entirely futile if your 'handler' is only 'handling' (and that Add is the only method you really need) - i.e. this fully depends on your code and structure and the implementation of things. If your base interface 'serves the purpose' for something other than that - then it might be worth it. Otherwise - you can do all that with object pretty much - and cast from object and you won't be any less or more happier about it.

NSGaga-mostly-inactive
  • 14,052
  • 3
  • 41
  • 51
0

Further expanding on my comment, your IResponseHandler<T> interface is contravariant on T (T appears in an "input" position). There is no way to do what you want because it is not type-safe.

To steal an analogy that Eric Lippert likes to use, if a banana is a fruit then it might seem reasonable to think that a bowl of bananas is bowl of fruit. However, this is only type-safe if you are asking what is in this bowl? If you try adding to the bowl it is all wrong. A bowl of fruit should be able to accept any fruit. However, if we can view your bowl of bananas as a bowl of fruit then you could add an orange to a bowl of bananas and have a mess.

The compiler is stopping you from being able to have that inconsistency. Your IResponseHandler<T> objects cannot accept any IResponse, only specific IResponse types.

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122
  • So what is a good way to accomplish this, then? Say I have a class that implements IResponseHandler and add it as a handler. When I receive a LoginResponse, I want to iterate through all registered handlers on that class and perform their callback, passing along the LoginResponse for the callback to use as it will. The interface will require an implementation something like void Handle(LoginResponse response) – Charlie Mulic Mar 30 '13 at 21:28
  • Add to the map lists of type `List>`. Since you are already doing lookups by type anyway just down cast the items in the list to `IResponseHandler` when you need to invoke `Handle`. – Mike Zboray Mar 30 '13 at 21:36