Ковариация в С#
Можно ли отличить List<Subclass>
до List<Superclass>
в С# 4.0?
Что-то в этом роде:
class joe : human {}
List<joe> joes = GetJoes();
List<human> humanJoes = joes;
Разве это не ковариация?
если вы можете сделать:
human h = joe1 as human;
почему бы вам не удастся сделать
List<human> humans = joes as List<human>;
чем это было бы неправомерно делать (joe) людям [0], потому что этот предмет был заброшен.. и все были бы счастливы. Теперь единственной альтернативой является создание нового списка
Ответы
Ответ 1
Вы не можете этого сделать, потому что это небезопасно. Рассмотрим:
List<Joe> joes = GetJoes();
List<Human> humanJoes = joes;
humanJoes.Clear();
humanJoes.Add(new Fred());
Joe joe = joes[0];
Очевидно, что последняя строка (если не предыдущая) должна завершиться неудачей - поскольку Fred
не является Joe
. Инвариантность List<T>
предотвращает эту ошибку во время компиляции вместо времени выполнения.
Ответ 2
Создайте новый человеческий список, который принимает joes как вход:
List<human> humanJoes = new List<human>(joes);
Ответ 3
Нет. Функции co/contravariance С# 4.0 поддерживают только интерфейсы и делегаты. Не поддерживаются конкретные типы, такие как List<T>
.
Ответ 4
Нет. Как сказал Джаред, функции co/contravariance С# 4.0 поддерживают только интерфейсы и делегаты. Однако он не работает с IList<T>
, и причина в том, что IList<T>
содержит методы добавления и изменения элементов в списке, как говорит новый ответ Джона Скита.
Единственный способ опубликовать список "joe" для "human" - это если интерфейс чисто предназначен только для чтения по дизайну, что-то вроде этого:
public interface IListReader<out T> : IEnumerable<T>
{
T this[int index] { get; }
int Count { get; }
}
Даже метод Contains(T item)
не будет разрешен, так как при нажатии IListReader<joe>
на IListReader<human>
в IListReader<joe>
нет метода Contains(human item)
.
Вы можете "принудительно" сделать из IList<joe>
в IListReader<joe>
, IListReader<human>
или даже IList<human>
с помощью GoInterface. Но если список достаточно мал, чтобы скопировать, более простое решение - просто скопировать его в новый List<human>
, как указывала Пау.
Ответ 5
Если я позволю вашему List<Joe> joes
быть обобщенным как...
List<Human> humans = joes;
... две ссылки humans
и joes
теперь, указывая на тот же список. Код, следующий за вышеуказанным назначением, не имеет возможности предотвратить вставку/добавление экземпляра другого типа человека, скажем, водопроводчика, в список. Учитывая, что class Plumber: Human {}
humans.Add(new Plumber()); // Add() now accepts any Human not just a Joe
список, к которому относится humans
, теперь содержит как joes, так и plumbers. Обратите внимание, что этот же объект списка все еще упоминается ссылкой joes
. Теперь, если я использую ссылку joes
для чтения из объекта списка, я могу вытащить водопроводчика вместо joe. Водопроводчик и Джо, как известно, неявно взаимопревращаемы... поэтому мое получение водопроводчика вместо joe из списка разрушает безопасность типа. Водопроводчик, безусловно, не приветствуется ссылкой на список joes.
Однако в последних версиях С# существует возможность обойти это ограничение для общего класса/коллекции, реализуя общий интерфейс, у которого в параметре type есть модификатор out
. Скажем, теперь у нас есть ABag<T> : ICovariable<out T>
. Модификатор out ограничивает только T-позиции (например, типы возврата метода). Вы не можете ввести T в сумку. Вы можете только их прочитать. Это позволяет нам обобщать joes на ICovariable<Human>
, не беспокоясь о том, чтобы вставить в него Plumber, поскольку интерфейс этого не позволяет. Теперь мы можем писать...
ICovariable<Human> humans = joes ; // now its good !
humans.Add(new Plumber()); // error