Contra / Co Variance для списка универсальных типов, предполагающих наследование базового интерфейса - PullRequest
0 голосов
/ 28 августа 2018

в .NET У меня есть три уровня наследования с использованием шаблонов с шаблоном вроде:

public interface IManager<TEvent> where TEvent : IEvent 
{ 
    void Foo( TEvent mySpecificEvent ); 
    //... 
}

class Manager<TEvent> : IManager<TEvent> where TEvent : IEvent
class NumericManager<TEvent> : Manager<TEvent> where TEvent : IEvent 
class SpecificNumericManager : NumericManager<ISpecificNumericEvent>

Таким образом, определенные менеджеры могут обрабатывать определенные типы с надеждой, что я смогу хранить несколько видов определенных типов в списке и сказать им всем, чтобы они выполняли операции Foo(TEvent mySpecificEvent) над своими конкретными событиями, пока событие наследуется от * 1005. *

Все прекрасно, когда, но я пытаюсь обработать свой SpecificNumericManager как IManager<IEvent>, чтобы добавить его к List<IManager<IEvent>>. Я получаю ошибку времени выполнения: «Невозможно привести объект типа SpecificNumericManager к типу IManager<IEvent>»

Обратите внимание, что мой код написан на VB, но я перевел на C # в надежде на дополнительную поддержку, поэтому, если я сделаю что-то маленькое, это можно объяснить этим.

Я экспериментировал и могу успешно привести свой SpecificNumericManager<ISpecificNumericEvent> к IManager<ISpecificNumericEvent>, но не напрямую к <Manager<IEvent>>

Кажется, что контравариантность и ковариантность родовых типов появлялись в моем первом колебании, когда я узнал, что я могу сделать, чтобы исправить это, но я не обнаружил, что уловка есть.

Любой вклад приветствуется. Спасибо!

Ответы [ 2 ]

0 голосов
/ 28 августа 2018

@ V0ldek уже подробно объяснил, почему это происходит. По существу, отношение наследования TDerived : TBase не распространяется на T<TDerived> : T<TBase>.

Возможным решением является объявление неуниверсального базового типа:

public interface IManager
{
    void Foo(IEvent nonSpecificEvent);
    //... 
}

public interface IManager<TEvent> : IManager
    where TEvent : IEvent
{
    void Foo(TEvent mySpecificEvent);
    //... 
}

Это позволяет вам объявить список как

List<IManager> _list;

... совместимость со всеми видами менеджеров, прямо или косвенно вытекающих из IManager и являющихся общими или нет.

Это приводит к 2 перегруженным версиям Foo.

Вы можете скрыть член, когда к нему обращаются через типы, отличающиеся от определяющего интерфейса с явной реализацией:

class Manager<TEvent> : IManager<TEvent> where TEvent : IEvent
{
    public void Foo(TEvent mySpecificEvent)
    {
        //...
    }

    // Explicit implementation
    void IManager.Foo(IEvent nonSpecificEvent)
    {
        //...
    }
}

В VB это можно сделать, объявив его приватным

Private Sub Foo(nonSpecificEvent As IEvent) Implements IManager.Foo

Public Interface IManager
    Sub Foo(nonSpecificEvent As IEvent)
End Interface


Public Interface IManager(Of TEvent As IEvent)
    Inherits IManager

    Overloads Sub Foo(mySpecificEvent As TEvent)
End Interface


Class Manager(Of TEvent As IEvent)
    Implements IManager(Of TEvent)

    Public Sub Foo(mySpecificEvent As TEvent) Implements IManager(Of TEvent).Foo
    End Sub

    ' Hidden  implementation
    Private Sub Foo(nonSpecificEvent As IEvent) Implements IManager.Foo
    End Sub
End Class
0 голосов
/ 28 августа 2018

Вам нужно, чтобы IManager был ковариантным.

interface IManager<out TEvent> where TEvent : IEvent

Таким образом, это назначение становится легальным:

IManager<IEvent> manager = new Manager<IMoreSpecificEvent>();

Ковариация, в самом простом смысле, указывает, что вам все равно, является ли параметр типа более производным, чем ограничение для такого назначения. Это означает, что, помимо прочего, нигде в ковариантной реализации (так в вашем менеджере Вы можете прочитать больше о ковариации в C # здесь .

EDIT:

Правильно, вот этот метод:

void Foo(TEvent mySpecificEvent);

разрушает наш маленький план здесь. Прямо сейчас ваш Manager не является ковариантным, и на то есть веская причина. Давайте рассмотрим список:

List<IManager<IEvent>> list;

и скажите, что это назначение допустимо ( это не , но давайте представим):

IManager<ISpecificNumericEvent> manager = new SpecificNumericManager();

list.Add(manager);

Что произойдет, если мы передадим этот список, чтобы выполнить некоторые операции с менеджером? Ну, все, что мы знаем, это то, что внутри есть вещи типа IManager<IEvent>. Это означает, что мы должны быть в состоянии передать его любой IEvent в метод Foo, верно?

class BaseEvent : IEvent {}

foreach(var manager in list)
{
    IEvent event = new BaseEvent();

    manager.Foo(event);
}

Но так как мы ранее присвоили SpecificNumericManager списку, мы пытаемся вызвать метод с подписью:

void Foo(TEvent event) where TEvent : ISpecificNumericEvent

с BaseEvent, который не реализует ISpecificNumericEvent. Бум, система типов разрушена в результате огненного взрыва, кошки и собаки живут вместе, массовая истерия.

Этот интерфейс не может быть ковариантным по причинам, указанным выше - C # просто запрещает обход системы типов ранее, запрещая ковариантное присвоение IManager<IEvent>. По тем же причинам плохая идея будет подавать события цикла в универсальные IManager<IEvent>, потому что просто нет идеи узнать, сможет ли конкретный менеджер обработать это конкретное событие во время компиляции. Вам нужно подумать о том, чего вы действительно пытаетесь достичь с помощью этого цикла. Обычно, когда правила ковариации / контравариантности нарушаются, это потому, что то, что вы хотите сделать, на самом деле не имеет смысла. Однако, если вы хотите сделать что-то сумасшедшее с типами в общем списке IManager<IEvent>, то у вас не будет системы типов на вашей стороне, и вам придется использовать рефлексию для достижения ваших коварных целей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...