foreach vs someList.ForEach () {}

167

Najwyraźniej istnieje wiele sposobów na iterację kolekcji. Ciekawe, czy są jakieś różnice lub dlaczego używałbyś jednej metody zamiast drugiej.

Pierwszy typ:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Inny sposób:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Przypuszczam, że zamiast anonimowego delegata, którego używam powyżej, miałbyś delegata wielokrotnego użytku, którego możesz określić ...

Bryce Fischer
źródło
1
Sugeruję przeczytanie bloga Erica Lippertsa „foreach” vs „ForEach”
Erik Philips

Odpowiedzi:

134

Jest jedna ważna i użyteczna różnica między nimi.

Ponieważ .ForEach używa forpętli do iteracji kolekcji, jest to poprawne (edit: przed .net 4.5 - implementacja się zmieniła i oba rzucają):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

podczas gdy foreachużywa modułu wyliczającego, więc to nie jest poprawne:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: NIE kopiuj tego kodu do swojej aplikacji!

Te przykłady nie są najlepszymi praktykami, służą jedynie do pokazania różnic między ForEach()i foreach.

Usuwanie elementów z listy w forpętli może mieć skutki uboczne. Najczęstszy jest opisany w komentarzach do tego pytania.

Ogólnie rzecz biorąc, jeśli chcesz usunąć wiele elementów z listy, chciałbyś oddzielić określenie, które elementy usunąć, od faktycznego usunięcia. Nie zachowuje kompaktowości kodu, ale gwarantuje, że nie przegapisz żadnych elementów.


źródło
35
nawet wtedy powinieneś zamiast tego użyć someList.RemoveAll (x => x.RemoveMe)
Mark Cidade
2
Z Linq wszystko można zrobić lepiej.
2
RemoveAll () to metoda w List <T>.
Mark Cidade
3
Prawdopodobnie jesteś tego świadomy, ale ludzie powinni uważać na usuwanie elementów w ten sposób; jeśli usuniesz element N, iteracja pominie element (N + 1) i nie zobaczysz go w swoim delegacie ani nie będziesz miał okazji go usunąć, tak jakbyś zrobił to we własnej pętli for.
El Zorko,
9
Jeśli wykonasz iterację listy wstecz za pomocą pętli for, możesz usunąć elementy bez żadnych problemów z przesunięciem indeksu.
S. Tarık Çetin
78

Mieliśmy tutaj kod (w VS2005 i C # 2.0), w którym poprzedni inżynierowie robili wszystko, aby użyć list.ForEach( delegate(item) { foo;});zamiast foreach(item in list) {foo; };całego kodu, który napisali. np. blok kodu do odczytu wierszy z dataReadera.

Nadal nie wiem dokładnie, dlaczego to zrobili.

Wady list.ForEach()to:

  • W języku C # 2.0 jest to bardziej szczegółowe. Jednak w języku C # 3 i nowszych można użyć =>składni „ ”, aby utworzyć ładnie zwięzłe wyrażenia.

  • To jest mniej znane. Ludzie, którzy muszą utrzymywać ten kod, będą się zastanawiać, dlaczego zrobiłeś to w ten sposób. Zajęło mi trochę czasu, zanim zdecydowałem, że nie ma żadnego powodu, może poza tym, że pisarz wydaje się sprytny (jakość reszty kodu podważyła to). Było też mniej czytelne, z opcją „}) ” na końcu bloku kodu delegata.

  • Zobacz także książkę Billa Wagnera „Effective C #: 50 Specific Ways to Improve Your C #”, w której mówi o tym, dlaczego foreach jest preferowane od innych pętli, takich jak pętle for lub while - głównym punktem jest to, że pozwalasz kompilatorowi wybrać najlepszy sposób konstruowania pętla. Jeśli w przyszłej wersji kompilatora uda się zastosować szybszą technikę, otrzymasz ją za darmo, używając foreach i przebudowy zamiast zmiany kodu.

  • foreach(item in list)konstrukcja pozwala na wykorzystanie breaklubcontinue jeśli trzeba zamknąć lub iteracji pętli. Ale nie możesz zmieniać listy w pętli foreach.

Jestem zaskoczony, że list.ForEachjest nieco szybszy. Ale to prawdopodobnie nie jest uzasadniony powód, aby go używać przez cały czas, to byłaby przedwczesna optymalizacja. Jeśli Twoja aplikacja korzysta z bazy danych lub usługi internetowej, która nie jest sterowana w pętli, prawie zawsze będzie tam, gdzie mija czas. Czy porównałeś to również z forpętlą? Pliklist.ForEachMoże być szybciej ze względu na użyciu tego wewnętrznie i forpętli bez owijki będzie jeszcze szybciej.

Nie zgadzam się, że list.ForEach(delegate) wersja jest „bardziej funkcjonalna” w jakikolwiek istotny sposób. Przekazuje funkcję do funkcji, ale nie ma dużej różnicy w wyniku lub organizacji programu.

Nie sądzę, że foreach(item in list)„mówi dokładnie, jak chcesz to zrobić” - for(int 1 = 0; i < count; i++)pętla to robi, foreachpętla pozostawia wybór kontroli kompilatorowi.

Czuję, że w nowym projekcie będę używał foreach(item in list)większości pętli, aby zachować ich powszechne użycie i czytelność, i używać list.Foreach()tylko dla krótkich bloków, kiedy można zrobić coś bardziej elegancko lub zwięźle za pomocą =>operatora C # 3 " ". W takich przypadkach może już istnieć metoda rozszerzenia LINQ, która jest bardziej szczegółowa niż ForEach(). Zobacz, czy Where(), Select(), Any(), All(), Max()lub jedną z wielu innych metod LINQ już nie to, co chcesz zrobić z pętli.

Anthony
źródło
Dla ciekawości ... spójrz na implementację firmy Microsoft ... referenceource.microsoft.com/#mscorlib/system/collections/…
Alexandre
17

Dla zabawy wrzuciłem List do reflektora i oto wynikowy C #:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

Podobnie MoveNext w module wyliczającym, który jest używany przez foreach, to:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach jest znacznie mniejszy niż MoveNext - znacznie mniej przetwarzania - będzie bardziej prawdopodobne, że JIT stanie się czymś wydajnym.

Ponadto foreach () przydzieli nowy moduł wyliczający bez względu na wszystko. GC jest twoim przyjacielem, ale jeśli robisz to samo wielokrotnie, spowoduje to więcej wyrzuconych obiektów, w przeciwieństwie do ponownego użycia tego samego delegata - ALE - jest to naprawdę marginalna sprawa. W typowym użytkowaniu różnica jest niewielka lub żadna.

cokół
źródło
1
Nie masz gwarancji, że kod wygenerowany przez foreach będzie taki sam między wersjami kompilatora. Wygenerowany kod może zostać ulepszony w przyszłej wersji.
Anthony
1
Od wersji .NET Core 3.1 ForEach jest nadal szybszy.
jackmott
13

Znam dwie niejasne rzeczy, które je odróżniają. Idź do mnie!

Po pierwsze, istnieje klasyczny błąd polegający na tworzeniu delegata dla każdego elementu na liście. Jeśli użyjesz słowa kluczowego foreach, wszyscy twoi pełnomocnicy mogą w końcu odwołać się do ostatniej pozycji na liście:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

Metoda List.ForEach nie ma tego problemu. Bieżący element iteracji jest przekazywany przez wartość jako argument do zewnętrznej lambdy, a następnie wewnętrzna lambda poprawnie przechwytuje ten argument w swoim własnym zamknięciu. Problem rozwiązany.

(Niestety uważam, że ForEach jest członkiem List, a nie metodą rozszerzającą, chociaż łatwo jest to zdefiniować samodzielnie, więc masz tę funkcję na dowolnym wyliczalnym typie.)

Po drugie, podejście metodą ForEach ma pewne ograniczenia. Jeśli implementujesz IEnumerable przy użyciu zwrotu zysku, nie możesz wykonać zwrotu zysku wewnątrz lambda. Tak więc przeglądanie elementów kolekcji w celu uzyskania zwracanych rzeczy nie jest możliwe przy użyciu tej metody. Będziesz musiał użyć słowa kluczowego foreach i obejść problem zamknięcia, ręcznie wykonując kopię bieżącej wartości pętli wewnątrz pętli.

Więcej tutaj

Daniel Earwicker
źródło
5
Problem, o którym wspomniałeś, foreachzostał „naprawiony” w C # 5. stackoverflow.com/questions/8898925/ ...
StriplingWarrior
13

Wydaje mi się, że someList.ForEach()połączenie można łatwo zrównoleglać, podczas gdy normalne foreachnie jest łatwe do równoległego prowadzenia. Możesz łatwo uruchomić kilka różnych delegatów na różnych rdzeniach, co nie jest takie łatwe w przypadku normalnego foreach.
Tylko moje 2 centy

Joachim Kerschbaumer
źródło
2
Myślę, że miał na myśli, że silnik runtime może zrównoleglać go automatycznie. W przeciwnym razie zarówno foreach, jak i .ForEach mogą być równoległe ręcznie przy użyciu wątku z puli w każdym delegacie akcji
Isak Savo,
@Isak, ale byłoby to błędne założenie. Jeśli anonimowa metoda przyciągnie lokalnego lub członka, środowisko wykonawcze nie będzie łatwo 8) w stanie paralelizować
Rune FS
6

Jak mówią, diabeł tkwi w szczegółach ...

Największą różnicą między tymi dwoma metodami wyliczania kolekcji jest to, że foreachprzenosi 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 foreachnajpierw 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:

  1. Otrzymujemy obiekt stanowy z intymną wiedzą o podstawowej kolekcji.
  2. 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 foreachstyló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.

Stacy Dudovitz
źródło
5

Możesz nazwać anonimowego delegata :-)

A drugie możesz napisać jako:

someList.ForEach(s => s.ToUpper())

Które wolę i oszczędza dużo pisania.

Jak mówi Joachim, równoległość łatwiej zastosować do drugiej formy.

Craig.Nicol
źródło
2

W tle anonimowy delegat zostaje zamieniony w rzeczywistą metodę, więc możesz mieć trochę narzutu z drugim wyborem, jeśli kompilator nie zdecydował się na wbudowanie funkcji. Ponadto wszelkie zmienne lokalne, do których odwołuje się treść przykładu anonimowego delegata, zmieniłyby swoją naturę z powodu sztuczek kompilatora, aby ukryć fakt, że jest kompilowany do nowej metody. Więcej informacji o tym, jak C # robi tę magię:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx

jezell
źródło
2

Funkcja ForEach jest członkiem klasy ogólnej List.

Utworzyłem następujące rozszerzenie, aby odtworzyć kod wewnętrzny:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Więc na koniec używamy normalnego foreach (lub pętli, jeśli chcesz).

Z drugiej strony użycie funkcji delegata to po prostu kolejny sposób definiowania funkcji, ten kod:

delegate(string s) {
    <process the string>
}

jest równa:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

lub używając wyrażeń labda:

(s) => <process the string>
Pablo Caballero
źródło
2

Cały zakres ForEach (funkcja delegata) jest traktowany jako pojedynczy wiersz kodu (wywołanie funkcji) i nie można ustawiać punktów przerwania ani wchodzić do kodu. Jeśli wystąpi nieobsługiwany wyjątek, zaznaczany jest cały blok.

Peter Shen
źródło
2

List.ForEach () jest uważana za bardziej funkcjonalną.

List.ForEach()mówi, co chcesz zrobić. foreach(item in list)mówi też dokładnie, jak chcesz to zrobić. To pozostawia List.ForEachswobodę zmiany realizację how części w przyszłości. Na przykład, hipotetyczna przyszła wersja .Net może zawsze działaćList.ForEach równolegle, przy założeniu, że w tym momencie każdy ma pewną liczbę rdzeni procesora, które są zwykle bezczynne.

Z drugiej strony foreach (item in list)daje trochę większą kontrolę nad pętlą. Na przykład wiesz, że elementy będą iterowane w jakiejś kolejności sekwencyjnej i możesz łatwo złamać w środku, jeśli element spełni jakiś warunek.


Kilka nowszych uwag na ten temat jest dostępnych tutaj:

https://stackoverflow.com/a/529197/3043

Joel Coehoorn
źródło
1

Drugi sposób, który pokazałeś, używa metody rozszerzającej do wykonania metody delegata dla każdego elementu na liście.

W ten sposób masz kolejne wywołanie delegata (= metody).

Dodatkowo istnieje możliwość iteracji listy za pomocą pętli for .

EFrank
źródło
1

Należy uważać na to, jak wyjść z metody Generic .ForEach - zobacz tę dyskusję . Chociaż link wydaje się mówić, że ta droga jest najszybsza. Nie wiem dlaczego - można by pomyśleć, że byłyby równoważne po skompilowaniu ...

Chris Kimpton
źródło
0

Jest sposób, zrobiłem to w mojej aplikacji:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Możesz użyć jako przedmiotu z każdego

Dannick Bédard
źródło