简体   繁体   中英

Enumerable.Any() and possible multiple enumerations

Rider/ReSharper gives me possible multiple enumeration warning on this:

public void ProcessProductCodes(IEnumerable<string> productCodes) {
    if (productCodes.Any()) {
        DoStuff(productCodes);
    }
}

Is it a false positive, or does the Any() function indeed mess up enumeration of collection?

The IEnumerable interface represents a sequence of items that can be iterated over, but makes no assumptions about the origin of the sequence. For instance, it could be a database query. If that was the case, here you would make 2 calls to the database, one for checking if there is any item in the sequence and another one to pass them to the DoStuff function, which obviously is not optimal performance-wise, and ReSharper is warning you about this.

To avoid this problem, you have 2 different options. If the collection of items is already in memory, you could make it explicit by changing the signature of your function to:

public void ProcessProductCodes(ICollection<string> productCodes) { ... }

If you cannot guarantee this, you can do a .ToList() or .ToArray at the beginning of your function:

public void ProcessProductCodes(IEnumerable<string> productCodes) {
  var productCodesList = productCodes.ToList();
  if (productCodesList .Any()) {
      DoStuff(productCodesList );
  }

ReSharper will do this for you, just select the quick refactoring (usually with Alt+Enter ).

You could create a helper method (I've created this as an extension method) to ensure that it's only iterated once:

public static class LinqExtensions
{
    public static bool DoIfAny<T>(this IEnumerable<T> collection, Action<IEnumerable<T>> action)
    {
        var enumerator = collection.GetEnumerator();
        if (!enumerator.MoveNext())
        {
            return false;
        }

        action(CreateEnumerableFromStartedEnumerable(enumerator));
        return true;
    }

    private static IEnumerable<T> CreateEnumerableFromStartedEnumerable<T>(IEnumerator<T> enumerator)
    {
        do
        {
            yield return enumerator.Current;
        }
        while (enumerator.MoveNext());
    }
}

Essentially this will create an enumerator for the collection, and then try to move to the first item. If that fails, then the method is not called and false is returned.

If it's successful, then it will produce a new enumerable which iterates the rest of the source enumerable, yielding its values as it goes. This includes the very first value. This will then be passed to an action delegate and true will be returned.

Usage:

IEnumerable<string> values = new string[] { "a", "b", "c" };
bool delegateCalled = values.DoIfAny(e => DoStuff(e));
Console.WriteLine("Delegate called: " + delegateCalled.ToString());

Try it online

Note that this will only really work if you want .Any() in the sense of "the collection isn't empty". If it is checking for the existence of a specific element, then you'll have to materialise the list first as in Tao's answer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM