C#: Iterator vs function returning IEnumerable

Viewed 349

Let's say a have two methods:

IEnumerable<int> DoSomething1();
IEnumerable<int> DoSomething2();

that modify the state of my object. I don't know if the function uses yield inside or just returns a List<int>.

And then I'd like to take the output and pass to two other functions:

void SendToUser(IEnumerable<int> values);
void PrintOut(IEnumerable<int> values);

Then just based on the function interface (DoSomethingX) I cannot say if this is a valid operation or not:

var values = DoSomethingX();
SendToUser(values);
PrintOut(values)

Because in the case of iterator it will result in calling DoSomethingX twice.

Is this some kind of inconsistency or I'm using iterator/IEnumerable in the wrong way? Where is the problem?

3 Answers

If a method returns IEnumerable then you should only rely on the fact that it is iterable, nothing more, as the implementation of that method is subject to change.

If you have control over the method and know that consumers require a more specific type, then return a more specific type.

If you don't have control and need to implement list behaviour, you could always convert the IEnumerable into a list:

var values = DoSomethingX().ToList();
SendToUser(values);
PrintOut(values);

Read about CQS (command query separation): Wikipedia

In short, there are 2 types of methods:

  1. commands, e.g. void DoSomething(string data) - they change state of your instance
  2. queries, e.g IEnumerable<Person> GetPeople(Filter filter) - which does not change state, they just return some data

Sometimes commands can return value, i.e. PersonID CreatePerson(...);

So if you refactor your code to:

void DoSomething1();
void DoSomething2();
IEnumerable<int> GetData();

it will solve your problem.

The IEnumerable type is just an interface that allows you to iterate over some elements. You don't have idea what is inside concrete implementation.
Usually implementation of IEnumerable should not have any side effects, i.e. changing state of some objects.

If you are dealing with IEnumerable that changes state, you can always materialize it using .ToList() LINQ extension. Then you can pass it to any method w/o any extra side-effects.

Assigning var values = DoSomethingX(); does not enumerate the collection or iterator it returns.

  1. It may return a List<int>. In this case the list is assigned to values. More precisely: a reference to this list.

  2. If DoSomethingX is an iterator method, then C# creates an object of an anonymous class implementing IEnumerable<int> as a state machine. This object is assigned to values.

Therefore there is no difference in usage of these two IEnumerable<int>. It does not matter whether it is based on a collection or on a iterator method. Iteration usually starts in a for-each loop or when calling a LINQ method like ToList or ToArray.

You can iterate manually. First you have to get the enumerator:

IEnumerator<int> enumerator = values.GetEnumerator();

Then call the methods of this enumerator:

while (enumerator.MoveNext()) { // Enumeration starts here.
    int value = enumerator.Current;
    ...
}

When you iterate over IEnumerable or IEnumerable<T> twice, then of course its code runs twice. If you think that this might be time consuming, e.g. if the iterator queries a database or the file system, then you should call .ToList() before calling it twice:

var records = QueryDatabaseReturnsIEnumerable().ToList();
SendToUser(records);
PrintOut(records);

On the other hand, calling .ToList() when the enumerable is based on a collection, then you perform an unnecessary memory and time consuming copy operation.

Usually you can say from the context, whether this is necessary or not. Otherwise the best you could do, is to test for other interfaces:

IEnumerable<int> values = DoSomethingX();
if (!(values is IList<int>) && !(values is ICollection<int>)) {
    values = values.ToList();
}
SendToUser(values);
PrintOut(values);

But I doubt that this is a good practice. Interfaces should be consumed without worrying about their implementation. Doing so defies their purpose.

Related