Ковариация с С# Generics
Учитывая интерфейс IQuestion
и реализацию этого интерфейса AMQuestion
, предположим следующий пример:
List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed;
В этом примере получается, как и ожидалось, ошибка компиляции, указывающая на то, что два не одного типа. Но в нем указано, что существует явное преобразование. Поэтому я изменяю его так:
List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;
Которая затем компилируется, но во время выполнения nonTyped
всегда имеет значение null. Если кто-то может объяснить две вещи:
- Почему это не работает.
- Как я могу добиться желаемого эффекта.
Было бы очень благодарно. Спасибо!
Ответы
Ответ 1
Тот факт, что AMQuestion
реализует интерфейс IQuestion
, не преобразуется в List<AMQuestion>
из List<IQuestion>
.
Поскольку этот приведение является незаконным, оператор as
возвращает null
.
Вы должны отнести каждый элемент отдельно:
IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();
Что касается вашего комментария, рассмотрите следующий код с обычными примерами животных-клише:
//Lizard and Donkey inherit from Animal
List<Lizard> lizards = new List<Lizard> { new Lizard() };
List<Donkey> donkeys = new List<Donkey> { new Donkey() };
List<Animal> animals = lizards as List<Animal>; //let pretend this doesn't return null
animals.Add(new Donkey()); //Reality unravels!
если нам было разрешено использовать List<Lizard>
для List<Animal>
, тогда мы могли бы теоретически добавить в этот список новый Donkey
, который нарушил бы наследование.
Ответ 2
Почему это не работает: as
возвращает null
, если динамический тип значения не может быть присвоен целевому типу, а List<AMQuestion>
не может быть отправлен на IList<IQuestion>
.
Но почему это не так? Ну, проверьте это:
List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;
nonTyped.Add(new OTQuestion());
AMQuestion whaaaat = typed[0];
IList<IQuestion>
говорит: "Вы можете добавить мне все IQuestion
". Но это обещание, которое оно не могло удержать, если бы оно было List<AMQuestion>
.
Теперь, если вы не хотели ничего добавлять, просто просмотрите его как коллекцию IQuestion
-совместимых вещей, тогда лучше всего сделать это с помощью IReadOnlyList<IQuestion>
с List.AsReadOnly
. Поскольку список, доступный только для чтения, не может иметь странных вещей, добавленных в него, он может быть правильно установлен.
Ответ 3
Проблема заключается в том, что List<AMQuestion>
нельзя отнести к IList<IQuestion>
, поэтому использование оператора as
не помогает. Явное преобразование в этом случае означает лить AMQuestion
в IQuestion
:
IList<IQuestion> nonTyped = typed.Cast<IQuestion>.ToList();
Кстати, у вас есть термин "ковариация" в вашем названии. В IList
тип не является ковариантным. Именно поэтому актерский состав не существует. Причина в том, что интерфейс IList
имеет T
в некоторых параметрах и в некоторых возвращаемых значениях, поэтому для T
нельзя использовать in
и out
. (@Sneftel имеет хороший пример, чтобы показать, почему это приведение не допускается.)
Если вам нужно только прочитать из списка, вы можете вместо этого использовать IEnumerable
:
IEnumerable<IQuestion> = typed;
Это будет работать, потому что IEnumerable<out T>
имеет out
, потому что вы не можете передать ему параметр T
as. Обычно вы должны сделать самое слабое "обещание" в своем коде, чтобы оно было расширяемым.
Ответ 4
IList<T>
не ковариантно для T
; это не может быть, поскольку интерфейс определяет функции, которые принимают значения типа T
в позиции "ввода". Однако IEnumerable<T>
ковариантно для T
. Если вы можете ограничить свой тип IEnumerable<T>
, вы можете сделать это:
List<AMQuestion> typed = new List<AMQuestion>();
IEnumerable<IQuestion> nonTyped = typed;
Это не делает никаких преобразований в списке.
Причина, по которой вы не можете преобразовать List<AMQuestion>
в List<IQuestion>
(при условии, что AMQuestion реализует интерфейс), заключается в том, что для таких функций, как List<T>.Add
, должно быть несколько проверок времени выполнения, чтобы убедиться, что вы действительно добавляете AMQuestion
.
Ответ 5
Оператор "as" всегда будет возвращать значение null там, где не существует допустимого действия - это определено поведение. Вы должны преобразовать или сделать список следующим образом:
IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();
Ответ 6
Тип с типичным параметром типа может быть только ковариантным, если этот общий тип встречается только в обращениях чтения и контравариантности, если он встречается только в обращениях к записи. IList<T>
позволяет как читать, так и записывать доступ к значениям типа T
, поэтому он не может быть вариантом!
Предположим, что вам разрешено присваивать List<AMQuestion>
переменной типа IList<IQuestion>
. Теперь давайте реализуем class XYQuestion : IQuestion
и вставляем значение этого типа в наш IList<IQuestion>
, который кажется совершенно законным. Этот список по-прежнему ссылается на List<AMQuestion>
, но мы не можем вставить XYQuestion
в List<AMQuestion>
! Поэтому два типа списка не совместимы с назначением.
IList<IQuestion> list = new List<AMQuestion>(); // Not allowed!
list.Add(new XYQuestion()); // Uuups!
Ответ 7
Поскольку List<T>
не является закрытым классом, возможно существование типа, который наследует от List<AMQuestion>
и реализует IList<IQuestion>
. Если вы не реализуете такой тип самостоятельно, крайне маловероятно, что он когда-либо будет существовать. Тем не менее, было бы вполне законно говорить, например,
class SillyList : List<AMQuestion>, IList<IQuestion> { ... }
и явно реализовать все члены типа IList<IQuestion>
. Таким образом, было бы также вполне законно говорить: "Если эта переменная содержит ссылку на экземпляр типа, полученного из List<AMQuestion>
, и если этот тип экземпляра также реализует IList<IQuestion>
, преобразуйте ссылку на последний тип.