Список Generics - неявное приведение - PullRequest
7 голосов
/ 01 августа 2011

Я нацеливаюсь на .NET 3.5. Допустим, у меня есть класс Боб, который является абстрактным базовым классом для SubBob.

Я могу заявить это:

Bob b = new SubBob();

Но я не могу этого сделать:

 // compliation error - can't convert
BindingList<Bob> myList = new BindingList<SubBob>(); 

Я предполагаю, что BindingList не хочет, чтобы вы это делали, потому что он должен знать, что тип, который находится справа, имеет ту же структуру памяти, что и левая сторона. SubBob может иметь больший размер, чем Bob.

Есть ли способ, которым я могу сделать неявное преобразование, или требуется приведение?

Ответы [ 5 ]

6 голосов
/ 01 августа 2011

Краткий ответ

Создавая экземпляр BindingList<SubBob>, вы ограничиваете его работу с SubBob и более конкретными типами (например, SubSubBob).

Если вы хотите, чтобы Bob также поместился там, объявите myList как список наименее определенного типа, который вы хотите поддерживать :

BindingList<Bob> myList = new BindingList<Bob>(); 

(или, что более удобно,)

var myList = new BindingList<Bob>(); 

Объяснение

Речь идет не о памяти (BindingList будет содержать только ссылку на объект, и все ссылки имеют одинаковый размер), скорее речь идет о логической несогласованности, которую вы привели бы.

Если бы такой код был возможен, вы бы могли произвольно нарушить ограничения типа :

BindingList<Animal> myList = new BindingList<Cat>(); 
myList.Add(new Dog()); // bang!

myList - это список Cat с, как вы ожидаете, что он будет обрабатывать Dog?

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

Дополнительная информация о совместной и контравариантности

Правильно, что в .NET 4.0, общая ковариация и контравариантность были добавлены для делегатов и интерфейсов ( не для классов ). Например, IEnumerable<out T> является ковариантным, что означает, что вы можете присвоить его переменной любого типа менее производной , чем T:

IEnumerable<Cat> cats = new List<Cat> { new Cat("Hudson"), new Cat("Crookshanks") };
IEnumerable<Animal> animals = cats; // sequence of cats is sequence of animals

Но это возможно только потому, что IEnumerable<out T> гарантирует, что возвращает только T (ключевое слово out), а никогда не принимает . Если бы он принял T в качестве параметра, он открыл бы проблему, описанную выше. По этой причине ICollection является , а не ковариантным.

Аналогичным образом, некоторые интерфейсы гарантируют, что они принимают только T (ключевое слово in) и никогда не возвращают его. Такие интерфейсы называются контравариантными и позволяют присваивать переменную с более конкретно T:

IComparer<Animal> animalComparer = // ...
IComparer<Dog> dogComparer = animalComparer; // comparer of animals is comparer of dogs
1 голос
/ 01 августа 2011

Если вы застряли в .NET 3.5 и не можете изменить уровень доступа к данным для предоставления чего-то более удобного, вы можете использовать довольно грязное преобразование:

BindingList<SubBob> subBobs = new BindingList<SubBob>;
BindingList<Bob> bobs = new BindingList(subBobs.Cast<Bob>().ToList())

Помимо посредственного дизайна, это вводит стоимость восстановления списка из IEnumerable<> в ToList(). BindingList<> конструктор, с другой стороны, только оборачивает список.

1 голос
/ 01 августа 2011

Причина, по которой ваш код не работает, заключается в том, что генерики не наследуют иерархию своих аргументов: BindingList<SubBob> не является производным от BindingList<Bob>.Это не имеет ничего общего с их макетами памяти.

A BindingList<Bob> может уже содержать SubBob объектов (и все остальное, происходящее из Bob), поэтому на самом деле может не потребоваться BindingList<SubBob>.Если по какой-то причине является причиной, по которой вам нужны оба варианта, единственным «простым» вариантом может быть использование неуниверсального интерфейса IBindingList.

.NET 4 поддерживает интерфейс covariance , который позволяет преобразовывать общие интерфейсы только для чтения в один интерфейс с базовым классом, поэтому он может или не может быть полезен в вашей ситуации ... то есть, если вы не застряли с 3.5:

IEnumerable<Bob> myList = new BindingList<SubBob>();
1 голос
/ 01 августа 2011

Есть ли способ сделать неявное преобразование или приведение требуется

Следующее должно позволить любому классу, который наследует Боба, быть добавленным в список как Боб.

BindingList<Bob> myList = new BindingList<Bob>();  

Что именно вы пытаетесь сделать?

1 голос
/ 01 августа 2011

Относительно вашего случая :
Вы имеете дело с контравариантностью здесь, и класс может поддерживать только ковариацию ИЛИ контравариантность . IEnumerable<T> поддерживает ковариацию.

Его подпись IEnumerable<out T>, поэтому она поддерживает унаследованные классы класса в T.

В общем :
К сожалению, это не доступно в .NET 3.5.

Он был представлен в .NET 4 как Ковариация и Контравариантность .

На MSDN есть часто задаваемые вопросы . Вот выдержка:

// Assignment compatibility. 
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type. 
object obj = str;

// Covariance. 
IEnumerable<string> strings = new List<string>();

// Contravariance.           
// Assume that I have this method: 
// static void SetObject(object o) { } 
Action<object> actObject = SetObject;
...