IEnumerable interface in C # and LSP

Original author: Vladimir Khorikov
  • Transfer
This article is a continuation of the C # article : read-only collections and LSPs . Today we look at the IEnumerable interface in terms of the Barbara Liskov substitution principle (LSP), and also see if this code violates IEnumerable code.

LSP and IEnumerable Interface


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

We can claim that the LSP is violated if one of the following conditions is met:
  • A subclass of the class (or, in our case, the interface) does not preserve the invariants of the parent
  • Subclass relaxes parent's postconditions
  • Subclass strengthens parent preconditions

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

IEnumerable Interface Implementations


Before we dive into the implementation, let's take a look at the interface itself. Here is the IEnumerable Interfaces Code, IEnumeratorand IEnumerator. IEnumerable interface is practically no different from IEnumerable.

public interface IEnumerable : IEnumerable
{
    IEnumerator GetEnumerator();
}
public interface IEnumerator : 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 revealing example would be implementation in the List class..

public class List
{
    public struct Enumerator : IEnumerator
    {
        private List 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 of type T does not require a call to MoveNext (), while the Current property of type object requires:

public void Test()
{
    List.Enumerator enumerator = new List().GetEnumerator();
    int current = enumerator.Current; // Возврашает 0
    object current2 = ((IEnumerator)enumerator).Current; // Бросает exception
}

The Reset () method is also implemented differently. While list.Enumerator.Reset () faithfully puts Enumerator at the top of the list, iterators will not implement them at all, so the following code will not work:

public void Test()
{
    Test2().Reset(); // Бросает NotSupportedException
}
private IEnumerator Test2()
{
    yield return 1;
}

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

private IEnumerable Test2()
{
    yield break;
}

So an endless sequence of elements:

private IEnumerable 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 the item to the collection:

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

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

IEnumerable and LSP


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

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

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

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

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

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

To avoid problems, we can simply change the type of the input parameter to ICollection. 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 elements, which is often undesirable if you want to use the collection as a read-only collection. Prior to .Net 4.5, an IEnumerable interface was often used for this purpose.

While this seems like a good solution, it puts too much restriction on the interface finders.

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

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

The solution is obvious - use IReadOnlyList:

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

There is no reason to continue to use the IEnumerable interface in places where you expect the collection to be calculable (and you expect this in most cases). Interfaces IReadOnlyCollection and IReadOnlyListAdded in .Net 4.5 make this work a lot easier.

IEnumerable and LSP implementations


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

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

There is an obvious problem in this code: access to the database is delayed until the client code begins to iterate the resulting set. Because At this point, the connection to the database is closed, the appeal will result in an exception:

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

This implementation violates the LSP because The IEnumerable interface itself does not have any preconditions requiring an open connection to the database. Following this interface, you should be able to iterate IEnumerable regardless of whether such a connection exists. As we can see, the DbQuery class reinforced the preconditions IEnumerable and thus violated the LSP.

In general, this is not necessarily a sign of poor design. Lazy computing is a fairly common approach when working with databases. It allows you to perform multiple 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 the original article: IEnumerable interface in .NET and LSP

Also popular now: