📜 ⬆️ ⬇️

C #: read-only and LSP collections

Often, developers claim that the read-only collections in .NET violate the Barbara Liskov substitution principle . Is it so? No, this is not the case, because the IList interface contains the IsReadOnly flag. The exception is the Array class, it really violates the LSP principle since .NET 2.0. But let's understand everything in order.


History of read-only collections in .NET


The diagram shows how the read-only collections evolved in .NET from version to version:


')
As you can see, the IList interface contains two properties: IsReadOnly and IsFixedSize. The original idea was to break these two concepts. The collection could be a read-only collection, which meant that it could not be changed at all; on the other hand, the collection could also be a fixed size (fixed size), i.e. it was possible to change existing elements in it, but it was impossible to add or delete existing ones. In other words, collections with the IsReadOnly flag set to true were always IsFixedSize, but the IsFixedSize collections were not always IsReadOnly.

Thus, if you want to create your read-only collection, you would need to implement both properties (IsReadOnly and IsFixedSize) so that they return true. In BCL at the time of .NET 1.0 there were no built-in read-only collections, but the architects laid the foundation for future implementations. The original idea was that the developers could use such collections polymorphically, approximately as follows:

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


Of course, this is not the most convenient way to work with collections, but nevertheless it avoids exceptions without recognizing the class behind the interface. Thus, this design does not violate the LSP. Of course, no one has done such checks while working with the IList interface (including me), so you can hear so many allegations that the read-only collections violate the LSP.

.NET 2.0


After generics were added to .NET 2.0, the BCL team was able to build a new version of the interface hierarchy. They did some work by making the collection interfaces more understandable. In addition to transferring some members from IList <T> to the ICollection <T>, they decided to remove the IsFixedSize flag.

This was done because the arrays were the only class that needed this flag. The Array class was the only one who forbade adding new or deleting existing elements, but allowed modification of existing ones. The BCL team decided that the IsFixedSize flag introduced too much complexity without giving almost any value. Interestingly, they changed the implementation of the IsReadOnly flag for arrays in .NET 2.0, so that it no longer reflected the status quo:

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


The IsReadOnly flag returns true for the array, but the collection can still be changed. This is where the violation of the LSP principle occurs. If we have a method that accepts IList <int>, we cannot simply write such code:

 public void AddAndUpdate(IList<int> list) { if (list.IsReadOnly) { // No action return; } // Both add and update list[0] = 1; list.Add(1); } 


If we pass a ReadOnlyCollection <int> object to the method, then (as intended) nothing will happen, since The collection is a read only collection. On the other hand, the object of the List <int> class (again, as planned) will be changed: a new element will be added and an existing element will be changed. But if we pass an array, then nothing will happen, because arrays return true for the ICollection <T> .IsReadOnly property. And we can’t find out if we have the ability to update existing elements, except by checking the type behind the interface:

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


Thus, arrays violate the LSP. Notice that they violate this principle only if we work with generic (generic) interfaces.

Was this a mistake on the part of Microsoft? It was a compromise. It was a weighted decision: this architecture is simpler, but it breaks the LSP in one particular place.

.NET 4.5


Despite the fact that the hierarchy of interfaces has become simpler, it still had a significant drawback: you need to check the IsReadOnly flag every time in order to find out if the collection can be changed. This is not the way developers are used to. And in general, no one used this flag for these purposes. This property was used only in scenarios with automatic data binding: data binding was one-sided in case IsReadOnly returned true and double-sided in other cases.

For the rest of the scenarios, everyone simply used the IEnumerable <T> interface or the ReadOnlyCollection <T> class. In order to solve this problem, two new interfaces were added to .NET 4.5: IReadOnlyCollection <T> and IReadOnlyList <T>.

These interfaces were added to the existing ecosystem, so that architects could not prevent backward compatibility. That is why the ReadOnlyCollection <T> class implements the IList, IList <T> and IReadOnlyList <T> interfaces, and not just the IReadOnlyList <T> interfaces. Such a change would lead to errors in existing assemblies compiled on older versions of .NET. In order for them to work, developers would have to recompile them in a new version.

Rewrite everything from scratch


Despite the fact that the current state of affairs cannot be changed due to the requirements of backward compatibility, it is still interesting to think about how the hierarchy of collections could be formed today, taking into account accumulated knowledge.

I think she would look like this:



Here is what was done:
1) Non-generic (non-generic) interfaces have been removed because they do not add value to the big picture.
2) The IFixedList <T> interface has been added, so the Array class is no longer required to implement the IList <T> interface.
3) The ReadOnlyCollection <T> class has been renamed ReadOnlyList <T> because this is a more suitable name for it. Also, it is now inherited only from the IReadOnlyList <T> interface.
4) IsReadOnly and IsFixedSize flags are removed. They can be added for data binding scripts, but I deleted them to show that they are no longer needed for polymorphic collections.

LSP Question


BCL has an interesting example of code:

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


This is an implementation of the Count extension method for LINQ-to-objects from the Enumerable class. The incoming object here is tested for compatibility with the ICollection and ICollection <T> interfaces for counting the number of elements. Does this method violate the LSP principle?

No, it does not break. Despite the fact that the method checks an object for belonging to real classes, all of these classes have the same implementation of the Count property. In other words, the ICollection.Count and ICollection <T> .Count properties have the same postconditions as the expression that counts the number of elements in a while loop.

Link to original article: C # Read-Only Collections and LSP

Source: https://habr.com/ru/post/256605/


All Articles