Dostęp do zmodyfikowanego zamknięcia (2)

101

To jest rozszerzenie pytania z Access to Modified Closure . Chcę tylko sprawdzić, czy poniższe elementy są wystarczająco bezpieczne do użytku produkcyjnego.

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)
{
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(list); });
}

Wykonuję powyższe tylko raz dla każdego uruchomienia. Na razie wydaje się, że działa dobrze. Jak Jon wspomniał o sprzecznych z intuicją wynikach w niektórych przypadkach. Więc na co muszę tutaj uważać? Czy będzie dobrze, jeśli lista zostanie sprawdzona więcej niż raz?

wadliwy
źródło
18
Gratulacje, jesteś teraz częścią dokumentacji Resharper. confluence.jetbrains.net/display/ReSharper/ ...
Kongress
1
To było trudne, ale powyższe wyjaśnienie wyjaśniło mi jasno: Może się to wydawać poprawne, ale w rzeczywistości tylko ostatnia wartość zmiennej str będzie używana po każdym kliknięciu dowolnego przycisku. Powodem tego jest to, że foreach rozwija się do pętli while, ale zmienna iteracji jest definiowana poza tą pętlą. Oznacza to, że przed wyświetleniem okna komunikatu wartość str mogła już zostać zmieniona na ostatnią wartość w kolekcji ciągów.
DanielV,

Odpowiedzi:

159

Przed C # 5 musisz ponownie zadeklarować zmienną wewnątrz foreach - w przeciwnym razie jest ona współdzielona, ​​a wszystkie twoje programy obsługi będą używać ostatniego ciągu:

foreach (string list in lists)
{
    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(tmp); });
}

Co ważne, zwróć uwagę, że od C # 5 to się zmieniło, a konkretnie w przypadkuforeach , nie musisz już tego robić: kod w pytaniu działałby zgodnie z oczekiwaniami.

Aby pokazać, że to nie działa bez tej zmiany, rozważ następujące kwestie:

string[] names = { "Fred", "Barney", "Betty", "Wilma" };
using (Form form = new Form())
{
    foreach (string name in names)
    {
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        {
            MessageBox.Show(form, name);
        };
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    }
    Application.Run(form);
}

Uruchom powyższe przed C # 5 i chociaż każdy przycisk wyświetla inną nazwę, kliknięcie przycisku powoduje wyświetlenie „Wilma” cztery razy.

Dzieje się tak, ponieważ specyfikacja języka (ECMA 334 v4, 15.8.4) (przed C # 5) definiuje:

foreach (V v in x) embedded-statement jest następnie rozszerzany do:

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

Zauważ, że zmienna v(twoja list) jest zadeklarowana poza pętlą. Zatem zgodnie z regułami przechwyconych zmiennych, wszystkie iteracje listy będą miały wspólny uchwyt przechwyconej zmiennej.

Począwszy od C # 5, zostało to zmienione: zmienna iteracji ( v) jest objęta zakresem wewnątrz pętli. Nie mam odniesienia do specyfikacji, ale w zasadzie wygląda to następująco:

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

Re anulowanie subskrypcji; jeśli chcesz aktywnie anulować subskrypcję anonimowego programu obsługi, sztuczka polega na przechwyceniu samego modułu obsługi:

EventHandler foo = delegate {...code...};
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

Podobnie, jeśli potrzebujesz jednorazowej obsługi zdarzeń (na przykład Load itp.):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate {
  // ... code
  obj.SomeEvent -= bar;
};
obj.SomeEvent += bar;

To jest teraz samodzielna rezygnacja z subskrypcji ;-p

Marc Gravell
źródło
W takim przypadku zmienna tymczasowa pozostanie w pamięci do czasu zamknięcia aplikacji, aby służyć delegatowi, i nie jest zalecane robienie tego w przypadku bardzo dużych pętli, jeśli zmienna zajmuje dużo pamięci. Czy mam rację?
wadliwy
1
Pozostanie w pamięci przez czas, w którym są rzeczy (przyciski) związane ze zdarzeniem. Istnieje sposób na rezygnację z subskrypcji jednorazowych delegatów, który dodam do wpisu.
Marc Gravell
2
Ale żeby zakwalifikować się do twojego punktu: tak, przechwycone zmienne mogą rzeczywiście zwiększyć zakres zmiennej. Musisz uważać, aby nie uchwycić rzeczy, których się nie spodziewałeś ...
Marc Gravell
1
Czy mógłbyś zaktualizować swoją odpowiedź w odniesieniu do zmiany specyfikacji C # 5.0? Tylko po to, aby była świetną dokumentacją wiki dotyczącą pętli foreach w języku C #. Istnieją już dobre odpowiedzi dotyczące zmiany w kompilatorze C # 5.0, który traktuje pętle foreach bit.ly/WzBV3L , ale nie są one zasobami podobnymi do wiki.
Ilya Ivanov
1
@Kos tak, forjest niezmieniony w wersji 5.0
Marc Gravell