Проблема с вариацией С#: присвоение списка <Производные> в виде списка <Base>

Посмотрите на следующий пример (частично взятый из Блог MSDN):

class Animal { }
class Giraffe : Animal { }

static void Main(string[] args)
{
    // Array assignment works, but...
    Animal[] animals = new Giraffe[10]; 

    // implicit...
    List<Animal> animalsList = new List<Giraffe>();

    // ...and explicit casting fails
    List<Animal> animalsList2 = (List<Animal>) new List<Giraffe>();
}

Является ли это проблемой ковариации? Будет ли это поддерживаться в будущем выпуске С# и есть ли какие-нибудь умные способы обхода (используя только .NET 2.0)?

Ответ 1

Хорошо, что это не будет поддерживаться на С# 4. Существует фундаментальная проблема:

List<Giraffe> giraffes = new List<Giraffe>();
giraffes.Add(new Giraffe());
List<Animal> animals = giraffes;
animals.Add(new Lion()); // Aargh!

Храните жирафов в безопасности: просто скажите "нет" небезопасной дисперсии.

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

В С# 4 будет поддерживаться безопасная общая дисперсия, но только для интерфейсов и делегатов. Таким образом, вы сможете:

Func<string> stringFactory = () => "always return this string";
Func<object> objectFactory = stringFactory; // Safe, allowed in C# 4

Func<out T> ковариантно в T, потому что T используется только в выходной позиции. Сравните это с Action<in T>, который контравариантен в T, потому что T используется только там, где находится вход, делая это безопасным:

Action<object> objectAction = x => Console.WriteLine(x.GetHashCode());
Action<string> stringAction = objectAction; // Safe, allowed in C# 4

IEnumerable<out T> также является ковариантным, что делает его правильным в С# 4, как указано другими:

IEnumerable<Animal> animals = new List<Giraffe>();
// Can't add a Lion to animals, as `IEnumerable<out T>` is a read-only interface.

С точки зрения работы с этим в вашей ситуации на С# 2, вам нужно поддерживать один список, или вы были бы счастливы создать новый список? Если это приемлемо, List<T>.ConvertAll является вашим другом.

Ответ 2

Он будет работать в С# 4 для IEnumerable<T>, поэтому вы можете сделать:

IEnumerable<Animal> animals = new List<Giraffe>();

Однако List<T> не является ковариационной проекцией, поэтому вы не можете назначать списки, как вы это делали выше, потому что вы можете сделать это:

List<Animal> animals = new List<Giraffe>();
animals.Add(new Monkey());

Это явно недействительно.

Ответ 3

В терминах List<T>, боюсь, вам не повезло. Тем не менее,.NET 4.0/С# 4.0 добавляет поддержку ковариантных/контравариантных интерфейсов. В частности, IEnumerable<T> теперь определяется как IEnumerable<out T>, что означает, что параметр типа теперь ковариантен.

Это означает, что вы можете сделать что-то подобное в С# 4.0...

// implicit casting
IEnumerable<Animal> animalsList = new List<Giraffe>();

// explicit casting
IEnumerable<Animal> animalsList2 = (IEnumerable<Animal>) new List<Giraffe>();

Примечание. Типы массивов также были ковариантными (по крайней мере, с .NET 1.1).

Мне кажется, что стыдно, что поддержка дисперсии не была добавлена ​​для IList<T> и других подобных общих интерфейсов (или общих классов даже), но, ну, по крайней мере, у нас есть что-то.

Ответ 4

Ковариация/контравариантность не может быть поддержана в изменчивых коллекциях, о чем упоминали другие, поскольку невозможно гарантировать безопасность типа в обоих случаях во время компиляции; однако, можно сделать быстрое одностороннее преобразование в С# 3.5, если это то, что вы ищете:

List<Giraffe> giraffes = new List<Giraffe>();
List<Animal> animals = giraffes.Cast<Animal>().ToList();

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

В .NET 2.0 вы можете использовать ковариацию массива для упрощения кода:

List<Giraffe> giraffes = new List<Giraffe>();
List<Animal> animals = new List<Animal>(giraffes.ToArray());

Но имейте в виду, что вы на самом деле создаете здесь две новые коллекции.