[Перевод] C#: коллекции только для чтения и LSP
Часто разработчики утверждают, что read-only коллекции в .NET нарушают принцип подстановки Барбары Лисков. Так ли это? Нет, это не так, потому что IList интерфейс содержит флаг IsReadOnly. Исключением является класс Array, он действительно нарушает LSP принцип начиная с версии .NET 2.0. Но давайте разберемся во всем по порядку.История read-only коллекций в .NETНа диаграмме показано как read-only коллекции эволюционировали в .NET от версии к версии:
Как вы видите, интерфейс IList содержит два свойства: IsReadOnly и IsFixedSize. Изначальная идея была в том, чтобы разбить эти два понятия. Коллекция могла быть коллекцией только для чтения (read-only), что означало, что ее нельзя было изменить вообще никак; с другой стороны, коллекция так же могла быть фиксированного размера (fixed size), т.е. в ней можно было изменять существующие элементы, но добавлять новые или удалять имеющиеся было нельзя. Другими словами, коллекции с флагом IsReadOnly равным true всегда были IsFixedSize, но IsFixedSize коллекции не всегда были IsReadOnly.
Таким образом, если вы хотите создать свою коллекцию только для чтения, вам было бы необходимо имплементировать оба свойства (IsReadOnly и IsFixedSize) так, чтобы они возвращали true. В BCL во времена .NET 1.0 не было втроенных read-only коллекций, но архитекторы заложили фундаменд для будущих реализаций. Изначальный замысел был в том, что разработчики могли бы использовать такие коллекции полиморфно примерно следующим образом:
public void AddAndUpdate (IList list) { if (list.IsReadOnly) { // No action return; } if (list.IsFixedSize) { // Update only list[0] = 1; return; } // Both add and update list[0] = 1; list.Add (1); } Конечно, это не самый удобный способ работы с коллекциями, но тем не менее он позволяет избежать исключений не узнавая при этом класс, стоящий за интерфейсом. Таким образом, этот дизайн не нарушает LSP. Конечно, никто не делал подобных проверок во время работы с интерфейсом IList (включая меня), поэтому вы можете слышать столько утверждений о том, что read-only коллекции нарушают LSP.
.NET 2.0 После того как в .NET 2.0 были добавлены generics, команда BCL получила возможность построить новую версию иерархии интерфейсов. Они провели некоторую работу, сделав интерфейсы коллекций более понятными. Помимо того, что они перенесли некоторые члены из IList в ICollection, они решили удалить флаг IsFixedSize.Это было сделано потому, что массивы были единственным классом, которым этот флаг был нужен. Класс Array был единственным, кто запрещал добавлять новые или удалять имеющиеся элементы, но разрешал модификацию существующих. Команда BCL решила, что флаг IsFixedSize привносил слишком много сложности, не давая при этом почти никакой ценности. Интересно, что они изменили имплементацию флага IsReadOnly для массивов в версии .NET 2.0, так что он больше не отражал имеющееся положение вещей:
public void Test ()
{
int[] array = { 1 };
bool isReadOnly1 = ((IList)array).IsReadOnly; // isReadOnly1 is false
bool isReadOnly2 = ((ICollection
public void AddAndUpdate (IList
public void AddAndUpdate (IList
Было ли это ошибкой со стороны Microsoft? Это был компромисс. Это было взвешанное решение: такая архитектура проще, но при этом нарушает LSP в одном конкретном месте.
.NET 4.5
Несмотря на то, что иерархия интерфейсов стала проще, в ней все еще имелся существенный недостаток: вам необходимо каждый раз проверять флаг IsReadOnly для того, чтобы узнать можно ли изменить коллекцию. Это не тот способ, к которому привыкли разработчики. И в общем-то, никто не использовал этот флаг для этих целей. Это свойство использовалось только в сценариях с автоматическим data binding: data binding был односторонний в случае если IsReadOnly возвращал true и двусторонный в остальных случаях.Для остальных сценариев все просто использовали IEnumerable
Эти интерфейсы были добавлены в существующую экосистему, так что архитекторы не могли допустить поломки обратной совместимости. Вот почему класс ReadOnlyCollection
Переписать всё с нуля Не смотря на то, что имеющееся положение вещей нельзя изменить из-за требований обратной совместимости, все же интересно подумать о том, как иерархия коллекций могла бы быть сформирована сегодня, с учетом накопленных знаний.Я думаю, что она бы выглядела следующим образом:
Вот что было сделано:1) Необобщенные (non-generic) интерфейсы были удалены, т.к. они не добавляют ценности в общую картину.2) Был добавлен интерфейс IFixedList
Вопрос по LSP
В BCL есть интересный пример кода:
public static int Count
Нет, не нарушает. Несмотря на то, что метод проверяет объект на принадлежность к реальным классам, все эти классы имеют одинаковую имплементацию свойства Count. Другими словами, свойства ICollection.Count и ICollection
Ссылка на оригинал статьи: C# Read-Only Collections and LSP