Tripping on Code

Consequences of Multiple Enumeration of an IEnumerable

January 02, 2021

One of the many warnings that Resharper throws at us is Code Inspection: Possible multiple enumeration of IEnumerable. When I was younger and much less experienced, this was one of those warnings that I never fully understood. Many of my methods returned IEnumerables rather than any of it's derivations. I started invoking ToList every time resharper complained, without completely understanding the reason for the warning. I understood that an IEnumerable could be lazily evaluated and that it could hurt performance to evaluate it again and again. I did not realize until recently that because of lazy evaluation, we could get vastly different results.

Consider the code below:

using System;
using System.Linq;
using System.Collections.Generic;

public class Person
{
	public int Id { get; set; }

	public string Name { get; set; }

    public int Age { get; set; }
}

public class PersonGenerator
{
	private Random random = new Random();
	public IEnumerable<Person> GetRandomPeople(int maxCount)
	{
		var count = this.random.Next(maxCount);
		for (var i = 0; i < count; i++)
		{
			yield return new Person
            {
                Id = random.Next(int.MaxValue),
                Age = random.Next(86),
            };
		}
	}
}

public class Program
{
	public static void Main()
	{
		PersonGenerator personGenerator = new PersonGenerator();
		var randomPeople = personGenerator.GetRandomPeople(100);
		var count1 = randomPeople.Count();
		var count2 = randomPeople.Count();

		Console.WriteLine($"Count1 = {count1}, Count2 = {count2}");
	}
}

The code above outputs the following

Count1 = 4, Count2 = 34

Wait. What's happening here?

The GetRandomPeople method uses yield return. This causes it to return a type which is lazily evaluated. Every time the Count() method is invoked, The GetRandomPeople method is invoked again. Since that method returns a random number of items, the counts are different for every enumeration of the IEnumerable.

The lazy evaluation of the IEnumerable is a very powerful feature, especially paired with a yield return, however to quote a tired old adage, "With great power comes great responsibility".

How do I prevent this?

  • The first thing that should have been done is to realize that multiple enumeration was occuring and invoke ToList on it.
  • There is no reason for the GetRandomPeople method to either return an IEnumerable or use a yield return. The method should return a List, a ReadOnlyCollection an ImmutableList or any other concrete collection implementation.

Microsoft has published a set of Guidelines for Collections that is a must read for all developers.