Jak mówią, diabeł tkwi w szczegółach ...
Największą różnicą między tymi dwoma metodami wyliczania kolekcji jest to, że foreach
przenosi stan, a ForEach(x => { })
nie.
Ale zagłębmy się trochę głębiej, ponieważ jest kilka rzeczy, o których powinieneś wiedzieć, a które mogą wpłynąć na twoją decyzję, i są pewne zastrzeżenia, o których powinieneś wiedzieć podczas kodowania dla obu przypadków.
Wykorzystajmy List<T>
w naszym małym eksperymencie obserwację zachowania. W tym eksperymencie używam platformy .NET 4.7.2:
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
Powtórzmy to foreach
najpierw z :
foreach (var name in names)
{
Console.WriteLine(name);
}
Możemy to rozszerzyć na:
using (var enumerator = names.GetEnumerator())
{
}
Z wyliczającym w ręku zaglądając pod okładki otrzymujemy:
public List<T>.Enumerator GetEnumerator()
{
return new List<T>.Enumerator(this);
}
internal Enumerator(List<T> list)
{
this.list = list;
this.index = 0;
this.version = list._version;
this.current = default (T);
}
public bool MoveNext()
{
List<T> list = this.list;
if (this.version != list._version || (uint) this.index >= (uint) list._size)
return this.MoveNextRare();
this.current = list._items[this.index];
++this.index;
return true;
}
object IEnumerator.Current
{
{
if (this.index == 0 || this.index == this.list._size + 1)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
return (object) this.Current;
}
}
Dwie rzeczy stają się natychmiast oczywiste:
- Otrzymujemy obiekt stanowy z intymną wiedzą o podstawowej kolekcji.
- Kopia kolekcji jest kopią płytką.
Nie jest to oczywiście w żaden sposób bezpieczne. Jak wskazano powyżej, zmiana kolekcji podczas iteracji jest po prostu złym mojo.
Ale co z problemem, że kolekcja staje się nieważna podczas iteracji, poprzez manipulowanie kolekcją poza nami podczas iteracji? Najlepsze rozwiązania sugerują przechowywanie wersji kolekcji podczas operacji i iteracji oraz sprawdzanie wersji w celu wykrycia zmian w kolekcji bazowej.
Tutaj sprawy stają się naprawdę mętne. Zgodnie z dokumentacją Microsoft:
Jeśli w kolekcji zostaną wprowadzone zmiany, takie jak dodawanie, modyfikowanie lub usuwanie elementów, zachowanie modułu wyliczającego jest niezdefiniowane.
Co to znaczy? Tytułem przykładu, tylko dlatego, że List<T>
implementuje obsługę wyjątków, nie oznacza, że wszystkie kolekcje, które implementują, IList<T>
będą robić to samo. Wydaje się, że jest to wyraźne naruszenie zasady zastępowania Liskova:
Obiekty z nadklasy powinny być wymienialne na obiekty z jej podklas bez przerywania aplikacji.
Innym problemem jest to, że moduł wyliczający musi zaimplementować IDisposable
- oznacza to kolejne źródło potencjalnych wycieków pamięci, nie tylko jeśli wywołujący pomylił się, ale jeśli autor nie implementujeDispose
poprawnie wzorca.
Na koniec mamy problem dożywotni ... co się stanie, jeśli iterator jest prawidłowy, ale podstawowa kolekcja zniknęła? Teraz mamy migawkę tego, co było ... kiedy oddzielasz okres istnienia kolekcji i jej iteratorów, prosisz o kłopoty.
Przyjrzyjmy się teraz ForEach(x => { })
:
names.ForEach(name =>
{
});
To rozszerza się do:
public void ForEach(Action<T> action)
{
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int version = this._version;
for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
action(this._items[index]);
if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
Na uwagę zasługują następujące kwestie:
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
Ten kod nie przydziela żadnych modułów wyliczających (nic do Dispose
) i nie zatrzymuje się podczas iteracji.
Należy pamiętać, że powoduje to również wykonanie płytkiej kopii podstawowej kolekcji, ale kolekcja jest teraz migawką w czasie. Jeśli autor nie zaimplementuje poprawnie sprawdzenia, czy kolekcja się zmieniła lub nie stała się „przestarzała”, migawka jest nadal ważna.
To w żaden sposób nie chroni Cię przed problemem problemów na całe życie ... jeśli podstawowa kolekcja zniknie, masz teraz płytką kopię, która wskazuje na to, co było ... ale przynajmniej nie masz Dispose
problemu z radzić sobie z osieroconymi iteratorami ...
Tak, powiedziałem, że iteratory ... czasami warto mieć stan. Przypuśćmy, że chcesz zachować coś podobnego do kursora bazy danych ... może być najlepszym rozwiązaniem w przypadku wielu foreach
stylów Iterator<T>
. Osobiście nie podoba mi się ten styl projektowania, ponieważ jest zbyt wiele problemów życiowych, a ty polegasz na łaskach autorów kolekcji, na których polegasz (chyba że dosłownie piszesz wszystko sam od zera).
Zawsze jest trzecia opcja ...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
To nie jest seksowne, ale ma zęby (przepraszam Toma Cruise'a i film The Firm )
To twój wybór, ale teraz już wiesz i może być świadomy.