Почему 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, нет общего списка с фиксированным размером.