Reputation: 199
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>>.TryGetValue(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>>?
Upvotes: 2
Views: 594
Reputation: 14302
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 upcast
from 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);
...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.
Upvotes: 1
Reputation: 40818
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.
Upvotes: 0
Reputation: 35870
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
.
Upvotes: 1