C#: коллекции только для чтения и LSP

C#: коллекции только для чтения и LSP

275
ПОДЕЛИТЬСЯ

Нет, это не так , поэтому что IList интерфейс содержит флаг IsReadOnly. Исключением является класс Array, он вправду нарушает LSP принцип начиная с версии .NET 2.0. Нередко создатели говорят, что read-only коллекции в .NET нарушают принцип подстановки Барбары Лисков. Так ли это? Но давайте разберемся во всем по порядку.

История read-only коллекций в .NET
На диаграмме показано как read-only коллекции эволюционировали в .NET от версии к версии:

Как вы видите, интерфейс IList содержит два характеристики: IsReadOnly и IsFixedSize. в ней можно было изменять имеющиеся элементы, но добавлять новейшие либо удалять имеющиеся было нельзя. Изначальная мысль была в том, чтоб разбить эти два понятия. Иными словами, коллекции с флагом IsReadOnly равным true постоянно были IsFixedSize, но IsFixedSize коллекции не постоянно были IsReadOnly. Коллекция могла быть коллекцией лишь для чтения (read-only), что означало, что ее нельзя было поменять вообщем никак; с иной стороны, коллекция так же могла быть фиксированного размера (fixed size), т.е.

В BCL во времена .NET 1.0 не было втроенных read-only коллекций, но архитекторы заложили фундаменд для будущих реализаций. Таковым образом, раз вы желаете сделать свою коллекцию лишь для чтения, для вас было бы нужно имплементировать оба характеристики (IsReadOnly и IsFixedSize) так, чтоб они возвращали true. Изначальный план был в том, что создатели могли бы применять такие коллекции полиморфно приблизительно последующим образом:

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);
}

Естественно, это не самый удачный метод работы с коллекциями, но тем не наименее он дозволяет избежать исключений не узнавая при этом класс, стоящий за интерфейсом. Естественно, никто не делал схожих проверок во время работы с интерфейсом IList (включая меня), потому вы сможете слышать столько утверждений о том, что read-only коллекции нарушают LSP. Таковым образом, этот дизайн не нарушает LSP.

.NET 2.0
Кроме того, что они перенесли некие члены из IList<T> в ICollection<T>, они решили удалить флаг IsFixedSize. Опосля того как в .NET 2.0 были добавлены generics, команда BCL получила возможность выстроить новейшую версию иерархии интерфейсов. Они провели некую работу, сделав интерфейсы коллекций наиболее понятными.

Любопытно, что они изменили имплементацию флага IsReadOnly для массивов в версии .NET 2.0, так что он больше не отражал имеющееся положение вещей: Это было изготовлено поэтому, что массивы были единственным классом, которым этот флаг был нужен. Команда BCL решила, что флаг IsFixedSize привносил очень много трудности, не давая при этом практически никакой ценности. Класс Array был единственным, кто запрещал добавлять новейшие либо удалять имеющиеся элементы, но разрешал модификацию имеющихся.

public void Test()
{
int[] array = { 1 };
bool isReadOnly1 = ((IList)array).IsReadOnly; // isReadOnly1 is false
bool isReadOnly2 = ((ICollection<int>)array).IsReadOnly; // isReadOnly2 is true
}

Вот где происходит нарушение принципа LSP. Раз у нас есть способ, принимающий IList<int>, мы не можем просто написать таковой код: Флаг IsReadOnly возвращает true для массива, но при этом коллекцию все равно можно поменять.

public void AddAndUpdate(IList<int> list)
{
if (list.IsReadOnly)
{
// No action
return;
}

// Both add and update
list[0] = 1;
list.Add(1);
}

Но раз мы передадим массив, то ничего не произойдет, т.к. С иной стороны, объект класса List<int> (снова же, как и задумано) будет изменен: в нем будет добавлен новейший элемент и изменен имеющийся. массивы возвращают true для характеристики ICollection<T>.IsReadOnly. коллекция является коллекцией лишь для чтения. Раз мы передадим способу объект класса ReadOnlyCollection<int>, то (как и задумано) ничего не произойдет, т.к. И мы никак не можем выяснить, есть ли у нас возможность проапдейтить имеющиеся элементы, не считая как с помощью проверки типа, стоящего за интерфейсом:

public void AddAndUpdate(IList<int> list)
{
if (list is int[])
{
// Update only
list[0] = 1;
return;
}

if (list.IsReadOnly)
{
// No action
return;
}

// Both add and update
list[0] = 1;
list.Add(1);
}

Заметьте, что они нарушают этот принцип лишь в случае раз мы работаем с обобщенными (generic) интерфейсами. Таковым образом, массивы нарушают LSP.

Это был компромисс. Было ли это ошибкой со стороны Microsoft? Это было взвешанное решение: таковая архитектура проще, но при этом нарушает LSP в одном определенном месте.

.NET 4.5
Невзирая на то, что иерархия интерфейсов стала проще, в ней все еще имелся значимый недочет: для вас нужно каждый раз инспектировать флаг IsReadOnly для того, чтоб выяснить можно ли поменять коллекцию. Это свойство использовалось лишь в сценариях с автоматическим data binding: data binding был односторонний в случае раз IsReadOnly возвращал true и двусторонный в других вариантах. Это не тот метод, к которому привыкли создатели. И в общем-то, никто не употреблял этот флаг для этих целей.

Для других сценариев все просто употребляли IEnumerable<T> интерфейс или класс ReadOnlyCollection<T>. Для того, чтоб решить эту делему, в .NET 4.5 были добавлены два новейших интерфейса: IReadOnlyCollection<T> и IReadOnlyList<T>.

Схожее изменение привело бы к ошибкам в работе имеющихся сборок, скомпилированных на наиболее старенькых версиях .NET. Чтоб они заработали, разрабам пришлось бы перекомпилировать их в новейшей версии. Вот почему класс ReadOnlyCollection<T> реализует интерфейсы IList, IList<T> и IReadOnlyList<T>, а не просто IReadOnlyList<T>. Эти интерфейсы были добавлены в существующую экосистему, так что архитекторы не могли допустить поломки обратной сопоставимости.

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

Я думаю, что она бы смотрелась последующим образом:

Вот что было изготовлено:
1) Необобщенные (non-generic) интерфейсы были удалены, т.к. они не добавляют ценности в общую картину.
2) Был добавлен интерфейс IFixedList<T>, так что класс Array больше не должен имплементировать интерфейс IList<T>.
Так же, он сейчас наследуется лишь от интерфейса IReadOnlyList<T>. это наиболее подходящее для него имя. 3) Класс ReadOnlyCollection<T> был переименован в ReadOnlyList<T>, т.к.
Они могут быть добавлены для сценариев с data binding, но я удалил их чтоб показать, что они больше не необходимы для полиморфной работы коллекциями. 4) Удалены флаги IsReadOnly и IsFixedSize.

Вопросец по LSP
В BCL есть увлекательный пример кода:

public static int Count<T>(this IEnumerable<T> source)
{
ICollection<T> collection1 = source as ICollection<T>;
if (collection1 != null)
return collection1.Count;

ICollection collection2 = source as ICollection;
if (collection2 != null)
return collection2.Count;

int count = 0;
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
checked { ++count; }
}
return count;
}

Это имплементация способа-расширения Count для LINQ-to-objects из класса Enumerable. Нарушает ли этот способ принцип LSP? Входящий объект тут тестируется на сопоставимость с интерфейсами ICollection и ICollection<T> для подсчета количества частей.

Иными словами, характеристики ICollection.Count и ICollection<T>.Count имеют те же постусловия (postconditions), что и выражение, подсчитывающее количество частей в цикле while. Нет, не нарушает. Невзирая на то, что способ инспектирует объект на принадлежность к настоящим классам, все эти классы имеют схожую имплементацию характеристики Count.

Ссылка на оригинал статьи: C# Read-Only Collections and LSP habrahabr.ru

НЕТ КОММЕНТАРИЕВ

ОСТАВЬТЕ ОТВЕТ

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.