Wydajność sprawdzania zawartości kolekcji

Nierzadko w różnego rodzaju aplikacjach można spotkać się z kodem, który sprawdza, czy dana kolekcja zawiera jakieś elementy. W wielu przypadkach, gdy tablica lub lista jest pusta, to można wcześniej przerwać kod. Programiści otrzymują kilka możliwości na sprawdzenie tego warunku. Wydajnością najpopularniejszych rozwiązań zajmiemy się w tym artykule.

Count() == 0 vs Any()

Każda osoba programująca w C# spotkała się z LINQ – pomocniczymi klasami rozszerzającymi funkcjonalność innych klas. Jednymi z najczęściej wykorzystywanych są te, które rozszerzają interfejs IEnumerable. Dzięki nim dostajemy takie metody jak Select(), Count() czy Any(). Tu szczególnie będą nas interesować ostatnie dwie spośród wymienionych.

Count() == 0

Metoda Count() zwraca liczbę elementów znajdujących się w enumeracji, czyli np. liczbę elementów listy, ilość wywołań yield return pewnej metody czy liczbę par klucz-wartość w słowniku. Jednak jeśli zajrzymy do jej wnętrza odkryjemy, że zanim przystąpi do liczenia sprawdza kilka warunków:

if (source == null)
{
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}

if (source is ICollection<TSource> collectionoft)
{
    return collectionoft.Count;
}

if (source is IIListProvider<TSource> listProv)
{
    return listProv.GetCount(onlyIfCheap: false);
}

if (source is ICollection collection)
{
    return collection.Count;
}

int count = 0;
using (IEnumerator<TSource> e = source.GetEnumerator())
{
    checked
    {
        while (e.MoveNext())
        {
            count++;
        }
    }
}

return count;

Jak widać, kod najpierw sprawdza, czy to, co próbujemy policzyć, nie jest nullem, następnie czy nie implementuje jednego z interfejsów, które mają prostszą logikę (np. w przypadku listy przechowywana jest informacja o liczbie elementów w jej środku, więc nie trzeba ich za każdym razem liczyć od początku) i dopiero, gdy żaden ze skrótów nie zadziała, uruchamiane jest „manualne” liczenie elementów.

Any()

Metoda Any(), jak nazwa wskazuje, sprawdza, czy w środku enumeracji znajduje się jakikolwiek element. Dodatkowo ta sama metoda pozwala na sprecyzowanie warunków, które musi spełnić element, aby został zaliczony. Tym jednak zajmiemy się innym razem. Podobnie jak w przypadku poprzedniej metody, zajrzyjmy do środka. Tym razem jednak mamy dwie różne odpowiedzi.

Przed .NET 5.0

if (source == null)
{
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}        
using (IEnumerator e = source.GetEnumerator())
{
    return e.MoveNext();
}

Już na pierwszy rzut oka widać, że kod jest znacznie krótszy. Czy oznacza to, że jest lepszy (czyt. szybszy)? Zanim przejdziemy do benchmarków, zastanówmy się nad tym. W kodzie nie ma skrótów przechodzących do specyficznych przypadków, przez co nie tracimy czasu, gdy korzystamy ze zwyczajnej enumeracji. Z drugiej strony, jeśli faktycznie odpalamy ten kod np. na liście – będziemy musieli i tak zaalokować enumerator, co może negatywnie wpłynąć na wydajność.

.NET 5.0 wzwyż

if (source == null)
{
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (source is ICollection collectionoft)
{
    return collectionoft.Count != 0;
}
else if (source is IIListProvider listProv)
{
    // Note that this check differs from the corresponding check in
    // Count (whereas otherwise this method parallels it).  If the count
    // can't be retrieved cheaply, that likely means we'd need to iterate
     // through the entire sequence in order to get the count, and in that
     // case, we'll generally be better off falling through to the logic
     // below that only enumerates at most a single element.
     int count = listProv.GetCount(onlyIfCheap: true);
     if (count >= 0)
     {
         return count != 0;
     }
}
else if (source is ICollection collection)
{
    return collection.Count != 0;
}
using (IEnumerator e = source.GetEnumerator())
{
    return e.MoveNext();
}

W najnowszej wersji .NET zostały wprowadzone pewne skróty. Dzięki nim można przede wszystkim uniknąć alokacji enumeratora, co ma ogromny wpływ na wydajność.