Почему generic IList <> не наследует не-общий IList
IList<T>
не наследует IList
, где IEnumerable<out T>
наследует IEnumerable
.
Если модификатор out
является единственной причиной, то почему большая часть реализации IList<T>
(например, Collection<T>
, List<T>
) реализует интерфейс IList
.
Итак, любой может сказать "ОК", если эти утверждения верны для всей реализации IList<T>
, а затем при необходимости направить его на IList
. Но проблема в том, что хотя IList<T>
не наследует IList
, поэтому не гарантируется, что каждый объект IList<T>
IList
.
Кроме того, использование IList<object>
, очевидно, не является решением, потому что без out
генераторы-генераторы не могут быть назначены классу less inherit; и создание нового экземпляра List не является решением здесь, потому что кому-то может понадобиться фактическая ссылка IList<T>
в качестве указателя IList
; и использование List<T>
insteed IList<T>
на самом деле является плохой практикой программирования и не служит целям.
Если .NET хочет предоставить гибкость в том, что каждая реализация IList<T>
не должна иметь контракт с нестандартной реализацией (т.е. IList
), то почему они не сохранили другой интерфейс, который реализует как общие, так и не общие и не предполагал, что весь конкретный класс, который хочет заключить контракт на родовой и негенический элемент, должен сжиматься через этот интерфейс.
Такая же проблема возникает при литье ICollection<T>
в ICollection
и IDictionary<TKey, TValue>
в IDictionary
.
Ответы
Ответ 1
Как вы заметили, T
в IList<T>
не ковариантно. Как правило: любой класс, который может изменять свое состояние, не может быть ковариантным. Причина в том, что такие классы часто имеют методы, которые имеют T
как тип одного из своих параметров, например void Add(T element)
. И ковариантные параметры типа не допускаются во входных позициях.
Среди прочих причин были добавлены дженерики для обеспечения безопасности типов. Например, вы не можете добавить Elephant
в список Apple
. Если ICollection<T>
был расширять ICollection
, вы могли бы вызвать ((ICollection)myApples).Add(someElephant)
без ошибки времени компиляции, поскольку ICollection
имеет метод void Add(object obj)
, который, по-видимому, позволяет вам добавить любой объект в список, в то время как на практике вы можете добавлять объекты T
. Поэтому ICollection<T>
не расширяет ICollection
а IList<T>
не расширяет IList
.
Андерс Хейлсберг, один из создателей С#, объясняет это следующим образом:
В идеале все общие интерфейсы коллекции (например, ICollection<T>
, IList<T>
) наследуют от своих не общих ролей, чтобы экземпляры универсального интерфейса могли использоваться как с общим, так и с нестандартным кодом.
Как оказалось, единственным универсальным интерфейсом, для которого это возможно, является IEnumerable<T>
, потому что только IEnumerable<T>
является контравариантным [sic 1 ]: В IEnumerable<T>
параметр типа T
используется только в " вывода "(возвращаемые значения), а не в" входных "позициях (параметрах). ICollection<T>
и IList<T>
используют T
в обоих положениях ввода и вывода, и поэтому эти интерфейсы являются инвариантными.
1) IEnumerable<T>
является ко-вариантом
Так как.Net 4.5 есть IReadOnlyCollection<out T>
и IReadOnlyList<out T>
ковариационные интерфейсы. Но IList<T>
, ICollection<T>
и многие из классов списка и коллекции не реализуют и не расширяют их. Честно говоря, я считаю их не очень полезными, поскольку они определяют только Count
и this[int index]
.
Если бы я мог перепроектировать.Net 4.5 с нуля, я бы разделил интерфейс списка на ковариационный интерфейс IList<out T>
, доступный только для чтения, который включает Contains
и IndexOf
, а также изменяемый инвариантный интерфейс IMutableList<T>
. Затем вы можете наложить IList<Apple>
на IList<object>
. Я реализовал это здесь:
Коллекции M42 - коллекции Covariant, списки и массивы.
Ответ 2
Обратите внимание, что с 2012 года в .NET 4.5 и более поздних версиях существует ковариантный (out
модификатор) интерфейс,
public interface IReadOnlyList<out T>
см. его документацию.
Обычно такие типы коллекций, как List<YourClass>
, Collection<YourClass>
и YourClass[]
, реализуют IReadOnlyList<YourClass>
, а из-за ковариации также могут использоваться как IReadOnlyList<SomeBaseClass>
и в конечном итоге IReadOnlyList<object>
.
Как вы уже догадались, вы не сможете изменить свой список через ссылку IReadOnlyList<>
.
С помощью этого нового интерфейса вы можете избежать совместного использования IList. Однако у вас все еще будет проблема, что IReadOnlyList<T>
не является базовым интерфейсом IList<T>
.
Ответ 3
Создайте интерфейс MyIList<T>
и наследуйте его от IList<T>
и IList
:
public interface MyIList<T> : IList<T>, IList
{ }
Теперь создайте класс MySimpleList
и дайте ему реализовать MyIList<T>
:
public class MySimpleList<T> : MyIList<T>
{
public int Count
{
get { throw new NotImplementedException(); }
}
public bool IsFixedSize
{
get { throw new NotImplementedException(); }
}
public bool IsReadOnly
{
get { throw new NotImplementedException(); }
}
public bool IsSynchronized
{
get { throw new NotImplementedException(); }
}
public object SyncRoot
{
get { throw new NotImplementedException(); }
}
object IList.this[int index]
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public T this[int index]
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public void Add(T item)
{
throw new NotImplementedException();
}
public int Add(object value)
{
throw new NotImplementedException();
}
public void Clear()
{
throw new NotImplementedException();
}
public bool Contains(T item)
{
throw new NotImplementedException();
}
public bool Contains(object value)
{
throw new NotImplementedException();
}
public void CopyTo(T[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
public int IndexOf(T item)
{
throw new NotImplementedException();
}
public int IndexOf(object value)
{
throw new NotImplementedException();
}
public void Insert(int index, T item)
{
throw new NotImplementedException();
}
public void Insert(int index, object value)
{
throw new NotImplementedException();
}
public bool Remove(T item)
{
throw new NotImplementedException();
}
public void Remove(object value)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
}
Теперь вы можете легко увидеть, что вам нужно удвоить реализацию множества методов. Один для типа T и один для объекта. В обычных обстоятельствах вы хотите этого избежать. Это проблема со-дисперсии и противоречия.
Лучшее объяснение, которое вы можете найти (для этой конкретной проблемы с IList и IList есть статья статьи из Brad, уже упомянутая Джоном в комментариях вопрос.
Ответ 4
Хорошие ответы уже были даны.
Однако уведомление об IList:
Замечания MSDN IList:
"Реализации IList делятся на три категории: только для чтения, фиксированный размер и переменный размер. (...). Для общей версии этого интерфейса см.
System.Collections.Generic.IList<T>
".
Это немного вводит в заблуждение, потому что на общей стороне мы имеем IList<T>
как variable-size и IReadOnlyList<T>
как прочитанные - только с 4.5, но AFAIK, нет общего списка с фиксированным размером.