Я пытаюсь добавить более производный тип в качестве значения в словарь <..., base>, но получаю следующую ошибку:
Невозможно преобразовать HandleIntegrationEvent<TR>
в HandleIntegrationEvent<IBaseEvent>
Пример
using System.Threading.Tasks;
using System.Collections.Generic;
public interface IBaseEvent
{
string Name { get; }
}
public class UserCreatedEvent: IBaseEvent
{
public string Name { get; } = "UserCreatedEvent";
}
public delegate Task HandleIntegrationEvent<in TR>(TR @event) where TR: IBaseEvent;
public class IntegrationBus
{
private readonly IDictionary<string, HandleIntegrationEvent<IBaseEvent>> _listeners = new Dictionary<string, HandleIntegrationEvent<IBaseEvent>>();
public void RegisterEventListener<TR>(string @event, HandleIntegrationEvent<TR> listener) where TR: IBaseEvent
{
// ERROR: cannot convert from HandleIntegrationEvent<TR> to HandleIntegrationEvent<IBaseEvent>
_listeners.Add(@event, listener);
}
}
Я просто не могу этого понять и уже некоторое время пытаюсь понять проблему. Насколько я понимаю, общее ограничение должно гарантировать, что экземпляр реализовал интерфейс IBaseEvent.
А пока у меня просто ментальный блок
РЕДАКТИРОВАТЬ Нашел еще один отличный пост https://stackoverflow.com/a/12841831, объясняющий причину словарной ковариации и контравариантности. Но я все еще немного в замешательстве
Вам нужно изменить словарь, чтобы хранить Func<IBaseEvent, Task>
вместо HandleIntegrationEvent<IBaseEvent>
. Затем оберните прослушиватель Func<IBaseEvent, Task>
, чтобы привести событие к соответствующему типу при добавлении в словарь.
Обновленный код:
private readonly IDictionary<string, Func<IBaseEvent, Task>> _listeners = new Dictionary<string, Func<IBaseEvent, Task>>(); //add using System; if you get an Func<> error
public void RegisterEventListener<TR>(string @event, HandleIntegrationEvent<TR> listener) where TR : IBaseEvent
{
_listeners.Add(@event, (e) => listener((TR)e));
}
Обновлено: Добавлена проверка типа, чтобы убедиться, что событие имеет соответствующий тип перед вызовом прослушивателя.
private readonly IDictionary<string, Func<IBaseEvent, Task>> _listeners = new Dictionary<string, Func<IBaseEvent, Task>>(); //add using System; if you get an Func<> error
public void RegisterEventListener<TR>(string @event, HandleIntegrationEvent<TR> listener) where TR : IBaseEvent
{
_listeners.Add(@event, async (e) =>
{
if (e is TR typedEvent)
{
await listener(typedEvent);
}
});
}
Спасибо за указание на это, но можно было бы добавить проверку типа, чтобы справиться с такой потенциальной ситуацией InvalidCastException
Проблема в том, что у вас есть обратная дисперсия, это ковариация, а не контравариантность. Это сбивает с толку, потому что делегаты меняют дисперсию. А в данном случае этого добиться невозможно, поскольку сам делегат должен быть контравариантным, чтобы можно было использовать TR
в качестве параметра.
Другими словами: переменная (или, в данном случае, параметр Dictionary.Add
) типа HandleIntegrationEvent<IBaseEvent>
не может принимать делегат HandleIntegrationEvent<TR>
, но переменная HandleIntegrationEvent<TR>
может принимать HandleIntegrationEvent<IBaseEvent>
.
Предположим, что это правда, что он может принять такого делегата. У вас есть местоположение и функция:
HandleIntegrationEvent<IBaseEvent> somelocation;
public Task HandleUserCreatedEvent(UserCreatedEvent event)
{
..
}
Вы берете HandleIntegrationEvent<UserCreatedEvent>
и храните его там.
somelocation = HandleUserCreatedEvent;
Кто-то теперь извлекает делегата и передает ему другой тип.
await somelocation(new SomeOtherEvent());
Упс!
Так что это недопустимо. Но поскольку вы храните эти делегаты в более обобщенном виде Dictionary
, у вас нет другого выбора, кроме как выполнить проверку приведения во время выполнения и либо выдать исключение, либо вернуть null
.
Единственное, что вы можете сделать, чтобы предотвратить такую проверку, — это сохранить реальный объект события вместе с делегатом (закрывая его каким-то образом), и, следовательно, сам делегат знает, как обработать вызов. Но похоже, что это не сработает для вашего варианта использования.
Пока делегат не будет вызван с другим типом, реализующим
IBaseEvent
, и вы не получитеInvalidCastException
.