📜 ⬆️ ⬇️

IEnumerable interface in C # and LSP

This article is a continuation of the C # article : read-only and LSP collections . Today, we look at the IEnumerable interface in terms of Barbara Liskov’s (LSP) substitution principle , and we’ll see if this principle is violated by the code implementing IEnumerable.

LSP and IEnumerable interface


To answer the question of whether the successor classes of the IEnumerable LSP violate the principle, let's see how this principle can be violated.

We can claim that the LSP is broken if one of the following conditions is met:

The problem with the IEnumerable interface is that its preconditions and postconditions are not marked explicitly and are often interpreted incorrectly. The official contracts for the IEnumerable and IEnumerator interfaces do not really help us in this. Even more, the different implementations of the IEnumerable interface often contradict each other.

Implementing an IEnumerable Interface


Before we dive into the implementation, let's take a look at the interface itself. Here is the code for the interfaces IEnumerable <T>, IEnumerator <T> and IEnumerator. The IEnumerable interface is almost the same as IEnumerable <T>.
')
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { T Current { get; } } public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); } 


They are pretty simple. However, different BCL classes implement them differently. Perhaps the most illustrative example would be implementation in the List <T> class.

 public class List<T> { public struct Enumerator : IEnumerator<T> { private List<T> list; private int index; private T current; public T Current { get { return this.current; } } object IEnumerator.Current { get { if (this.index == 0 || this.index == this.list._size + 1) throw new InvalidOperationException(); return (object)this.Current; } } } } 


The Current property with type T does not require a call to MoveNext (), while the Current property with type object requires:

 public void Test() { List<int>.Enumerator enumerator = new List<int>().GetEnumerator(); int current = enumerator.Current; //  0 object current2 = ((IEnumerator)enumerator).Current; //  exception } 

The reset () method is also implemented in different ways. While List <T> .Enumerator.Reset () faithfully translates Enumerator to the top of the list, iterators do not implement them at all, so the following code will not work:

 public void Test() { Test2().Reset(); //  NotSupportedException } private IEnumerator<int> Test2() { yield return 1; } 

It turns out that the only thing we can be sure of when working with IEnumerable is that the IEnumerable <T> .GetEnumerator () method returns a non-null enumerator object. A class implementing IEnumerable can be as an empty set:

 private IEnumerable<int> Test2() { yield break; } 

So and the infinite sequence of elements:

 private IEnumerable<int> Test2() { Random random = new Random(); while (true) { yield return random.Next(); } } 

And this is not a made-up example. The BlockingCollection class implements IEnumerator in such a way that the calling thread is blocked on the MoveNext () method until some other thread adds an item to the collection:

 public void Test() { BlockingCollection<int> collection = new BlockingCollection<int>(); IEnumerator<int> enumerator = collection.GetConsumingEnumerable().GetEnumerator(); bool moveNext = enumerator.MoveNext(); // The calling thread is blocked } 

In other words, the IEnumerable interface makes no guarantees about the underlying set of elements, it does not even guarantee that this set is finite . All that he tells us is that this set can be somehow iterated.

IEnumerable and LSP


So, do LSPs violate IEnumerable implementation classes? Consider the following example:

 public void Process(IEnumerable<Order> orders) { foreach (Order order in orders) { // Do something } } 

In the case if the underlying type of the orders - List <Orders> variable, everything is in order: list items can be easily iterated. But what if orders really are an infinite generator that creates a new object every time you call MoveNext ()?

 internal class OrderCollection : IEnumerable<Order> { public IEnumerator<Order> GetEnumerator() { while (true) { yield return new Order(); } } } 

Obviously, the Process method does not work as intended. But will this be because the OrderCollection class violates the LSP? Not. OrderCollection meticulously follows the IEnumerable interface contract: it provides a new object every time it is asked for it.

The problem is that the Process method expects more from an object implementing IEnumerable than this interface promises . There is no guarantee that the underlying class for the orders variable is the final collection. As I mentioned earlier, orders can be an instance of the BlockingCollection class, which makes it useless to try to iterate over all its elements.

To avoid problems, we can simply change the type of the input parameter to ICollection <T>. Unlike IEnumerable, ICollection provides the Count property, which ensures that the underlying collection is finite.

IEnumerable and read-only collections


Using ICollection has its drawbacks. ICollection allows you to change your items, which is often undesirable if you want to use the collection as a read-only collection. Prior to .Net 4.5, the IEnumerable interface was often used for this purpose.

While this seems like a good solution, it imposes too big restrictions on the sub-interfaces.

 public int GetTheTenthElement(IEnumerable<int> collection) { return collection.Skip(9).Take(1).SingleOrDefault(); } 

This is one of the most commonly used approaches: using LINQ to bypass IEnumerable. Despite the fact that such code is quite simple, it has one obvious disadvantage: it iterates the collection 10 times, while the same result can be achieved by simply reversing the index.

The solution is obviously to use IReadOnlyList:

 public int GetTheTenthElement(IReadOnlyList<int> collection) { if (collection.Count < 10) return 0; return collection[9]; } 

There is no reason to continue using the IEnumerable interface in places where you expect the collection to be countable (and you expect it in most cases). The IReadOnlyCollection <T> and IReadOnlyList <T> interfaces added in .Net 4.5 make this work much easier.

Implementing IEnumerable and LSP


What about IEnumerable implementations that violate the LSP? Let's take a look at an example in which the underlying IEnumerable <T> type is DbQuery <T>. We can get it as follows:

 private IEnumerable<Order> FindByName(string name) { using (MyContext db = new MyContext()) { return db.Orders.Where(x => x.Name == name); } } 

There is an obvious problem in this code: the access to the database is postponed until the client code starts to iterate the resulting set. Since At this point, the connection to the database is closed, the call will result in an exception:

 public void Process(IEnumerable<Order> orders) { foreach (Order order in orders) // Exception: DB connection is closed { } } 

Such an implementation violates the LSP, because IEnumerable in itself has no preconditions requiring an open connection to the database. Following this interface, you should be able to iterate over IEnumerable, regardless of whether such a connection exists. As we can see, the DbQuery class reinforces the IEnumerable preconditions and thus violates the LSP.

In general, this is not necessarily a sign of poor design. Lazy computing is a fairly common approach when working with a database. It allows you to perform several queries in one call to the database and thus increases the overall system performance. The price here is a violation of the LSP principle.

Link to original article: IEnumerable interface in .NET and LSP

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


All Articles