Пожалуйста, помогите мне понять полиморфизм при использовании дженериков в С#.

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

public interface IMyInterface
{
    void MyMethod();
}

public class MyClass : IMyInterface
{
    public void MyMethod()
    {
    }
}

public class MyContainer<T> where T : IMyInterface
{
    public IList<T> Contents;
}

Затем я могу сделать это, что отлично работает:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());

У меня есть много классов, реализующих MyInterface. Я хотел бы написать метод, который может принимать все объекты MyContainer:

public void CallAllMethodsInContainer(MyContainer<IMyInterface> container)
{
    foreach (IMyInterface myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

Теперь я хотел бы вызвать этот метод.

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

Это не сработало. Конечно, поскольку MyClass реализует IMyInterface, я должен быть в состоянии просто привести его?

MyContainer<IMyInterface> newContainer = (MyContainer<IMyInterface>)container;

Это тоже не сработало. Я определенно могу привести обычный MyClass к IMyInterface:

MyClass newClass = new MyClass();
IMyInterface myInterface = (IMyInterface)newClass;

Так что, по крайней мере, я не совсем неправильно это понял. Я точно не знаю, как мне написать метод, который принимает общий набор классов, соответствующих одному и тому же интерфейсу.

У меня есть план полностью обойти эту проблему, если потребуется, но я действительно предпочел бы сделать это правильно.

Заранее спасибо.


person Steve Rukuts    schedule 25.08.2010    source источник
comment
Именно здесь люди произносят такие страшные слова, как ковариантность и контравариантность.   -  person Greg    schedule 25.08.2010
comment
@Greg: С положительной стороны, я чувствую, что мое собственное понимание этих концепций в последнее время действительно конкретизировалось из-за множества подобных вопросов, которые появляются!   -  person Dan Tao    schedule 25.08.2010
comment
Концепты хорошие, но названия пугают. :)   -  person Greg    schedule 25.08.2010
comment
@ Грег: Согласен. Я предлагаю изменить их на поедание и извержение.   -  person Dan Tao    schedule 25.08.2010


Ответы (3)


Примечание. Во всех случаях вам придется инициализировать поле Contents конкретным объектом, реализующим IList<?>.

Когда вы сохраняете общее ограничение, вы можете сделать:

public IList<T> Contents = new List<T>();

Когда вы этого не сделаете, вы можете сделать:

public IList<MyInterface> Contents = new List<MyInterface>();

Способ 1:

Измените метод на:

public void CallAllMethodsInContainer<T>(MyContainer<T> container) where T : IMyInterface
{
    foreach (T myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

и фрагмент:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

Метод 2:

В качестве альтернативы переместите метод CallAllMethodsInContainer в класс MyContainer<T> следующим образом:

public void CallAllMyMethodsInContents()
    {
        foreach (T myClass in Contents)
        {
            myClass.MyMethod();
        }
    }

и измените фрагмент на:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
container.CallAllMyMethodsInContents();

Способ 3:

РЕДАКТИРОВАТЬ: Еще одна альтернатива - удалить общее ограничение из класса MyContainer следующим образом:

public class MyContainer
{
    public IList<MyInterface> Contents;
}

и изменить сигнатуру метода на

  public void CallAllMethodsInContainer(MyContainer container)

Тогда фрагмент должен работать как:

MyContainer container = new MyContainer();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

Обратите внимание, что в этом варианте список Contents контейнера будет принимать любую комбинацию объектов, реализующих MyInterface.

person Ani    schedule 25.08.2010

Ого, в последнее время очень часто возникает этот вопрос.

Краткий ответ: Нет, это невозможно. Вот что возможно:

public void CallAllMethodsInContainer<T>(MyContainer<T> container) where T : IMyInterface
{
    foreach (IMyInterface myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

И вот почему то, что вы пытались невозможно (взято из этот мой недавний ответ):

Рассмотрим тип List<T>. Допустим, у вас есть List<string> и List<object>. строка происходит от объекта, но из этого не следует, что List<string> происходит от List<object>; если бы это было так, то у вас мог бы быть такой код:

var strings = new List<string>();

// If this cast were possible...
var objects = (List<object>)strings;

// ...crap! then you could add a DateTime to a List<string>!
objects.Add(new DateTime(2010, 8, 23));23));

Приведенный выше код иллюстрирует, что значит быть (и не быть) ковариантный тип. Обратите внимание, что приведение типа T<D> к другому типу T<B>, где D является производным от B, возможно (в .NET 4.0), если T является ковариантным; универсальный тип является ковариантным, если его аргумент универсального типа появляется только в форме вывода, т. е. в виде свойств, доступных только для чтения, и возвращаемых функцией значений.

Подумайте об этом так: если какой-то тип T<B> всегда поставляет B, то тот, который всегда предоставляет D (T<D>), сможет работать как T<B>, поскольку все D являются B.

Между прочим, тип является контравариантным, если его параметр универсального типа появляется только в виде входных данных, т. е. параметров метода. Если тип T<B> является контравариантным, то его можно привести к типу T<D>, как бы странно это ни звучало.

Подумайте об этом так: если какой-то тип T<B> всегда требует B, то он может заменить тип, для которого всегда требуется D, поскольку, опять же, все D являются B.

Ваш класс MyContainer не является ни ковариантным, ни контравариантным, потому что его параметр типа появляется в обоих контекстах — как вход (через Contents.Add) и как выход (через само свойство Contents).

person Dan Tao    schedule 25.08.2010

это проблема ковариации

http://msdn.microsoft.com/en-us/library/dd799517.aspx

вы не можете преобразовать MyContainer<MyClass> в MyContainer<IMyInterface>, потому что тогда вы могли бы делать такие вещи, как Contents.Add(new AnotherClassThatImplementsIMyInterface())

person Kikaimaru    schedule 25.08.2010