Почему я не могу назначить Список ‹Derived› списку ‹Base›?

Я определил следующий класс:

public abstract class AbstractPackageCall
    {

     ...

    }

Я также определяю подкласс этого класса:

class PackageCall : AbstractPackageCall
    {

      ...
    }

Есть также несколько других подклассов AbstractPackageCall

Теперь я хочу сделать следующий звонок:

 List<AbstractPackageCall> calls = package.getCalls();

Но у меня всегда бывает такое исключение:

Error   13  Cannot implicitly convert type 'System.Collections.Generic.List<Prototype_Concept_2.model.PackageCall>' to 'System.Collections.Generic.List<Prototype_Concept_2.model.AbstractPackageCall>' 

В чем проблема? Это метод Package # getCalls

 internal List<PackageCall> getCalls()
        {
            return calls;
        }

person Community    schedule 10.01.2011    source источник
comment
Это называется ковариацией / контравариантностью с Generics. Ознакомьтесь с этим вопросом SO.   -  person jro    schedule 11.01.2011
comment
Если вы можете вернуть IEnumerable<AbstractPackageCall> вместо List<AbstractPackageCall>, тогда это будет работать, поскольку IEnumerable<> поддерживает ковариацию.   -  person Russ Cam    schedule 11.01.2011
comment
Вам также может быть интересно прочитать эти два сайта. Прочитав это, я щелкнул мышью по взаимной / противоположной вариации ОСОБЕННОСТИ C # 4.0 - ЧАСТЬ 4 - СО- И КОНТРАВАРИАНТНОСТЬ ДЛЯ ОБЩЕГО ДЕЛЕГАТА И ТИПЫ ИНТЕРФЕЙСА и, конечно же, Эрик У Липперта много сообщений по этой теме.   -  person R0MANARMY    schedule 11.01.2011
comment
Спасибо за все ответы, наконец, я думаю, что понимаю концепцию ковариации.   -  person    schedule 11.01.2011


Ответы (5)


Самый простой способ понять, почему это запрещено, - это следующий пример:

abstract class Fruit
{
}

class Apple : Fruit
{
}

class Banana : Fruit
{
}

// This should intuitively compile right? Cause an Apple is Fruit.
List<Fruit> fruits = new List<Apple>();

// But what if I do this? Adding a Banana to a list of Apples
fruits.Add(new Banana());

Последнее утверждение разрушило бы безопасность типов .NET.

Однако массивы позволяют это:

Fruit[] fruits = new Apple[10]; // This is perfectly fine

Однако включение Banana в fruits все равно нарушит безопасность типов, поэтому .NET должен выполнять проверку типа при каждой вставке массива и генерировать исключение, если на самом деле это не Apple. Это потенциально (небольшое) снижение производительности, но это можно обойти, создав struct оболочку для любого типа, поскольку эта проверка не выполняется для типов значений (потому что они не могут наследовать ни от чего). Сначала я не понимал, почему было принято это решение, но вы довольно часто будете сталкиваться с тем, почему это может быть полезно. Чаще всего используется String.Format, который принимает params object[], и в него можно передать любой массив.

Однако в .NET 4 существует безопасная ковариация / контравариантность типов, которая позволяет выполнять некоторые подобные присваивания, но только в том случае, если они доказуемо безопасны. Что доказуемо безопасно?

IEnumerable<Fruit> fruits = new List<Apple>();

Вышеупомянутое работает в .NET 4, потому что IEnumerable<T> стало IEnumerable<out T>. out означает, что T может выходить только вне из fruits и что на IEnumerable<out T> нет никакого метода, который бы когда-либо принимал T в качестве параметра, поэтому вы никогда не сможете неправильно передать Banana в IEnumerable<Fruit>.

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

person JulianR    schedule 11.01.2011
comment
Почему голос против? Если ответ не является глупым, аргументированным или оскорбительным, всегда оставляйте комментарий, чтобы я мог исправить / исправить ситуацию. - person JulianR; 22.01.2011
comment
Это отличный ответ прямо сейчас. Честная игра для вас за то, что вы нашли время - person John Mc; 20.03.2013

Теперь, если вы хотите сделать следующий звонок:

List<PackageCall> calls = package.getCalls();

// select only AbstractPackageCall items
List<AbstractPackageCall> calls = calls.Select();
calls.Add(new AnotherPackageCall());

Я называю это обобщением.

Кроме того, вы можете использовать это более конкретно:

List<PackageCall> calls = package.getCalls();

// select only AnotherPackageCall items
List<AnotherPackageCall> calls = calls.Select(); 
calls.Add(new AnotherPackageCall());

Реализуйте это, используя метод расширения:

public static class AbstractPackageCallHelper
{
    public static List<U> Select(this List<T> source)
        where T : AbstractPackageCall
        where U : T
    {
        List<U> target = new List<U>();
        foreach(var element in source)
        {
            if (element is U)
            {
                 target.Add((U)element);
            }
        }
        return target;
    }
}
person Artur Mustafin    schedule 11.01.2011
comment
Вашему методу расширения требуются два параметра типа, и их нужно будет включить в вызовы метода. - person Eamon Nerbonne; 28.01.2011

Почему то, что вы пытаетесь, не работает

Вы просите компилятор рассматривать List<PackageCall> как List<AbstractPackageCall>, но это не так. Другое дело, что каждый из этих PackageCall экземпляров на самом деле является AbstractPackageCall.

Что будет работать вместо этого

var calls = package.getCalls().Cast<AbstractPackageCall>().ToList();

Почему то, что вы пытаетесь, никогда не сработает

Несмотря на то, что каждый элемент внутри возвращаемого значения package.getCalls() происходит от AbstractPackageCall, мы не можем рассматривать весь список как List<AbstractPackageCall>. Вот что бы произошло, если бы мы могли:

var calls = package.getCalls(); // calls is List<PackageCall>
List<AbstractPackageCalls> apcs = calls; // ILLEGAL, but assume we could do it

apcs.Add(SomeOtherConcretePackageCall()); // BOOM!

Если бы мы могли это сделать, то мы могли бы добавить SomeOtherConcretePackageCall к List<PackageCall>.

person Jon    schedule 10.01.2011

Потому что List<PackageCall> не наследуется от List<AbstractPackageCall>. Вы можете использовать метод расширения Cast<>(), например:

var calls = package.getCalls().Cast<AbstractPackageCall>();
person Brian Ball    schedule 10.01.2011
comment
Ошибка 3. Невозможно неявно преобразовать тип «System.Collections.Generic.IEnumerable‹ Prototype_Concept_2.model.AbstractPackageCall ›» в «System.Collections.Generic.List‹ Prototype_Concept_2.model.AbstractPackageCall ›». Существует явное преобразование (вам не хватает приведений?} - person RoflcoptrException; 11.01.2011
comment
Да, только что исправил мой ответ. - person Brian Ball; 11.01.2011

Вы не можете выполнить это преобразование, потому что List<AbstractPackageCall> может содержать все, что происходит от AbstractPackageCall, а List<PackageCall> не может - он может содержать только вещи, производные от PackageCall.

Подумайте, разрешено ли это приведение:

class AnotherPackageCall : AbstractPackageCall { }

// ...

List<AbstractPackageCall> calls = package.getCalls();
calls.Add(new AnotherPackageCall());

Вы только что добавили AnotherPackageCall в List<PackageCall>, но AnotherPackageCall не является производным от PackageCall! Вот почему это преобразование запрещено.

person cdhowie    schedule 10.01.2011
comment
Такое преобразование разрешено для массивов, вероятно, поэтому он спросил об этом. - person R0MANARMY; 11.01.2011