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ść.