Так много интерфейсов!

Сегодня будет еще один вопрос со StackOverflow, снова представленный в виде диалога.

В документации по List < T > в MSDN говорится, что класс объявлен следующим образом

 public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable

Действительно ли класс List < T > реализует все эти интерфейсы?

Да.

Почему их так много?

Поскольку когда такой интерфейс как IList<T> наследует от такого интерфейса как IEnumerable<T>, тогда класс, реализующий производный интерфейс должен также реализовать и предыдущие интерфейсы в цепочке наследования. Именно это означает наследование интерфейсов; если вы выполняете контракт производного типа, то это требует от вас выполнять контракт и предыдущих типов.

Так это значит, что класс или структура должны реализовывать все методы всех интерфейсов в транзитивном замыкании базовых интерфейсов?

Именно.

В классе (или структуре), которая реализует производный интерфейс также необходимо указать в списке базовых типов, что он реализует все предыдущие интерфейсы в цепи наследования?

Нет.

Так их не следует указывать?

Нет.

Так это значит, что указывать реализацию предыдущих интерфейсов в цепи наследования необязательно?

Да.

Всегда ?

Практически всегда:

 interface I1 {}
interface I2 : I1 {}
interface I3 : I2 {}

Необязательно указывать, наследует ли интерфейс I3 от I1 или нет.

 class B : I3 {}

Класс (или структура), реализующие I3 должны реализовывать интерфейсы I2 и I1, но не требуется указывать это. Это необязательно.

 class D : B {}

Классам-наследникам не нужно заново говорить о том, что они реализуют интерфейс своего базового класса, но это и не запрещено. (Это особый случай; подробности смотри ниже.)

 class C<T> where T : I3
{
public virtual void M<U>() where U : I3 {}
}

Типы аргументов, соответствующие T и U должны реализовывать интерфейсы I2 и I1, однако указывать это в ограничениях для T и U не обязательно.

Необязательно заново указывать любые базовые интерфейсы в частичных классах:

 partial class E : I3 {}
partial class E {}

Во второй части объявления класса E может быть указана реализация интерфейсов I3, I2 или I1, но делать это не обязательно.

Хорошо, я понял, это делать не обязательно. Так зачем кому-то понадобиться явно указывать базовые интерфейсы?

Возможно потому, что автор кода считает, что это сделает код проще для понимания и более самодокументированным.

Или, возможно, разработчик написал следующий код:

 interface I1 {}
interface I2 {}
interface I3 : I1, I2 {}

и понял, ага, погодите минутку, интерфейс I2 должен наследоваться от I1. Почему это изменение должно требовать от разработчика вернуться и изменить объявление I3, и исключить явное упоминание об I1?! Я не вижу причин заставлять разработчиков удалять избыточную информацию.

Помимо упрощения чтения и понимания кода, существует ли техническая разница между явным указанием интерфейсов в списке базовых типов или не указанием этого?

Обычно нет, но в одном случае могут быть тонкие отличия. Предположим, у вас есть класс-наследник D, чей базовый класс B реализует некоторые интерфейсы. Класс D автоматически реализует эти интерфейсы через класс B. Если вы заново укажите интерфейсы в списке базовых типов класса D, тогда компилятор C# выполнит повторную реализацию интерфейсов (interface reimplementation). Подробности этого не очевидны; если вам интересно, как это работает, тогда я вам рекомендую внимательно прочитать раздел 13.4.6 спецификации языка C# 4. Идея в основном заключается в том, что компилятор «все начинает сначала» и определяет, какие методы какие интерфейсы реализует.

Действительно ли в исходном коде класса List < T > указаны все эти интерфейсы?

Нет. Реальный исходный код выглядит так:

 public class List<T> : IList<T>, System.Collections.IList

Тогда почему в документации MSDN приведет полный список интерфейсов, а в исходном коде нет?

Потому что MSDN – это документация; она должна дать вам максимум информации. Гораздо яснее, когда максимально полная документация содержится в одном месте, вместо того, чтобы искать нужную информацию по десятку разных страниц, чтобы выяснить полный набор интерфейсов.

Тогда почему такие интерфейсы, как Reflector или objectbrowser показывают полный список?

У этих инструментов нет исходного кода. Они опираются только на метаданные. Поскольку полное указание списка интерфейсов является необязательным, инструмент не может знать, содержит ли исходный код весь список или нет. Так что лучше ошибиться в сторону избытка информации. Опять-таки, инструмент старается помочь вам, показывая вам больше информации, нежели скрывая необходимую часть информации от вас.

Я обратил внимание, что интерфейс IEnumerable < T > наследует IEnumerable , но IList < T > не наследует от IList . Почему?

По той же причине, по которой интерфейс IEnumerable<T> может быть сделан ковариантным по T, а IList<T> - нет. Последовательность целых чисел может рассматриваться, как последовательность объектов, путем упаковки каждого целого, по мере получения его из последовательности. Но список целых чисел, доступный для чтения/записи, не может рассматриваться, как список объектов, доступный для чтения/записи, поскольку вы можете поместить строку в список объектов. Интерфейс IEnumerable<T> легко может следовать контракту IEnumerable путем простого вспомогательного метода, упаковывающего значения. Но интерфейс IList<T> не может следовать всему контракту IList, вот поэтому он от него и не наследует.

Тогда почему List < T > реализует IList ?

Это немного странно, поскольку класс List<T> для любого типа кроме object не удовлетворяет контракту IList. Возможно, это сделано для упрощения работы тем, кто переходит от старого кода на C# 1.0 к обобщенным коллекциям; эти люди уже вероятно убедились в том, что они помещают в список только объекты правильного типа. И чаще всего вы передаете IList для того, чтобы вызываемый код обращался к списку по индексу, а не для того, чтобы он добавлял элементы произвольного типа.

Оригинал статьи