Czy istnieje powód ponownego użycia zmiennej w foreach przez C#?

Używając wyrażeń lambda lub anonimowych metod w C#, musimy uważać na dostęp do zmodyfikowanego zamknięcia pułapki. Na przykład:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Ze względu na zmodyfikowane zamknięcie powyższy kod spowoduje, że wszystkie Where klauzule dotyczące zapytania będą oparte na ostatecznej wartości s.

Jak wyjaśniono tutaj , dzieje się tak, ponieważ s zmienna zadeklarowana w foreach pętli powyżej jest przetłumaczona w ten sposób w kompilator:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

Zamiast tak:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Jak wspomniano tutaj, nie ma żadnych korzyści z deklarowania zmiennej poza pętlą, a w normalnych okolicznościach jedynym powodem, dla którego mogę myśleć o tym, jest to, że planujesz użyć zmiennej poza zakresem pętli:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Zmienne zdefiniowane w pętli foreach nie mogą być używane poza pętlą:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Więc kompilator deklaruje zmienną w sposób, który sprawia, że jest bardzo podatny na błędy, które często są trudne do znalezienia i debugowania, przy jednoczesnym braku dostrzegalnych korzyści.

Czy jest coś, co można zrobić z foreach pętlami w ten sposób, czego nie można zrobić, gdyby były skompilowane ze zmienną o wewnętrznym zasięgu, czy jest to po prostu arbitralny wybór, który został dokonany zanim anonimowe metody i wyrażenia lambda były dostępne lub powszechne, i który nie został zmieniony od tego czasu?

Author: Community, 2012-01-17

4 answers

Kompilator deklaruje zmienną w sposób, który sprawia, że jest ona bardzo podatna na błędy, które często są trudne do znalezienia i debugowania, nie generując przy tym żadnych zauważalnych korzyści.

Twoja krytyka jest całkowicie uzasadniona.

Omawiam ten problem szczegółowo tutaj:

Zamknięcie zmiennej pętli za szkodliwą

Czy jest coś, co można zrobić z pętlami foreach w ten sposób, czego nie można zrobić, jeśli zostały skompilowane z wewnętrznym zakresem zmienna? czy jest to po prostu arbitralny wybór, który został dokonany zanim anonimowe metody i wyrażenia lambda były dostępne lub powszechne, i który nie został zmieniony od tego czasu?

Ten ostatni. Specyfikacja C# 1.0 nie mówi, czy zmienna loop znajduje się wewnątrz, czy poza ciałem pętli, ponieważ nie ma zauważalnej różnicy. Kiedy semantyka closure została wprowadzona w C# 2.0, dokonano wyboru, aby umieścić zmienną loop poza pętlą, zgodnie z "for" pętla. Myślę, że można powiedzieć, że wszyscy żałują tej decyzji. Jest to jeden z najgorszych "gotchas" w C# i przyjmiemy przełomową zmianę, aby to naprawić. W C # 5 zmienna pętli foreach będzie logicznie wewnątrz ciała pętli, a więc closures otrzyma za każdym razem nową kopię.

Pętla for nie zostanie zmieniona, a zmiana nie zostanie "przeniesiona z powrotem" do poprzednich wersji C#. Dlatego należy zachować ostrożność, gdy używając tego idiomu.

 1427
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-11 16:20:55

To, o co prosisz, zostało dokładnie omówione przez Erica Lipperta w jego wpisie na blogu zamykającym zmienną pętli uważaną za szkodliwą i jej kontynuacją.

Dla mnie najbardziej przekonującym argumentem jest to, że posiadanie nowej zmiennej w każdej iteracji byłoby niezgodne z pętlą stylu for(;;). Czy spodziewasz się, że w każdej iteracji for (int i = 0; i < 10; i++) pojawi się nowa int i?

Najczęstszym problemem z tym zachowaniem jest zamknięcie nad zmienną iteracji i ma łatwy obejście:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Mój wpis na blogu o tym problemie: Zamknięcie nad zmienną foreach w C # .

 192
Author: Krizz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-28 21:36:36

Ugryziony przez to, mam zwyczaj włączania lokalnie zdefiniowanych zmiennych w najbardziej wewnętrznym zakresie, którego używam do przeniesienia do dowolnego zamknięcia. W twoim przykładzie:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Robię:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        
Kiedy już masz ten nawyk, możesz go uniknąć w bardzo rzadkim przypadku, w którym zamierzałeś związać się z zewnętrznymi lunetami. Szczerze mówiąc, nigdy tego nie robiłem.
 105
Author: Godeke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-01-26 14:13:22

W C # 5.0, ten problem jest naprawiony i można zamknąć zmienne pętli i uzyskać oczekiwane wyniki.

Specyfikacja języka mówi:

8.8.4 twierdzenie foreach

(...)

Wypowiedź foreach postaci

foreach (V v in x) embedded-statement

Jest następnie rozszerzony do:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
      … // Dispose e
  }
}

(...)

Umieszczenie v wewnątrz pętli while jest ważne dla tego, jak jest przechwytywane przez dowolną funkcję anonimową występującą w embedded-statement. Na przykład:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Jeśli v zostanie zadeklarowana poza pętlą while, będzie współdzielona wśród wszystkich iteracji, a jego wartość po pętli for byłaby wartość końcowa, 13, czyli to, co wyświetli wywołanie f. Zamiast tego, ponieważ każda iteracja ma swoją zmienną v, ta przechwycony przez f w pierwszej iteracji będzie nadal trzymał wartość 7, czyli to, co zostanie wydrukowane. (Uwaga: wcześniejsze wersje C# zadeklarowana v Poza pętlą while.)

 63
Author: Paolo Moretti,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-03 13:58:51