Проблема с вариацией С#: присвоение списка <Производные> в виде списка <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());
Но имейте в виду, что вы на самом деле создаете здесь две новые коллекции.