Search code examples
c#linqresharperienumerableside-effects

Will LINQ Any() ever enumerate without arguments?


Given the following:

var nums = GetNums();
Console.WriteLine(nums.Any());
Console.WriteLine(string.Join(", ", nums));
    
static IEnumerable<int> GetNums()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

ReSharper flags this as "Possible Multiple Enumeration." However, running it produces

True
1, 2, 3

proving that no elements were skipped over. Will this always be the case when calling Any without a predicate? Or are there cases where this will produce unwanted side effects?


Solution

  • It entirely depends on what's backing the IEnumerable<T> and how it operates, but you will iterate over it multiple times. Some enumerables will result in different results each time you enumerate, some won't (as in your example).

    For example, BlockingCollection<T> allows you to obtain a "consuming enumerable" which yields items only once.

    Example code:

    var blockingCollection = new BlockingCollection<int>();
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
    blockingCollection.CompleteAdding();
    
    var nums = blockingCollection.GetConsumingEnumerable();
    Console.WriteLine(nums.Any());
    Console.WriteLine(string.Join(", ", nums));
    Console.WriteLine(nums.Any());
    

    Output:

    True
    2, 3
    False
    

    Try it online

    Another possible issue is that a second enumeration of nums could execute a (potentially heavy) query against a database a second time, if the IEnumerable<T> represents the results of a database query. This could both result in additional unnecessary work, tying up unnecessary resources of your application and your database server.

    The data could also change between queries. Imagine this scenario:

    1. Someone inserts a record into your database.
    2. You check nums.Any() which makes one query, and indicates that there is a matching record.
    3. That record gets deleted.
    4. You call string.Join(", ", nums) but its now empty because there are no records in the database the second time the query is run.

    If you're intending to use an enumerable multiple times, or you're worried the data could change between enumerations, you can materialise it to an array, list, or other in-memory collection. Note that .ToList() will iterate over the enumerable to produce the list.

    For example:

    var blockingCollection = new BlockingCollection<int>();
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
    blockingCollection.CompleteAdding();
    
    var nums = blockingCollection.GetConsumingEnumerable()
        .ToList(); // put the values in a list
    Console.WriteLine(nums.Any());
    Console.WriteLine(string.Join(", ", nums));
    Console.WriteLine(nums.Any());
    

    Output:

    True
    1, 2, 3
    True