2009-11-02 15 views
7

W poniższym programie, DummyMethod zawsze drukuje 5. Ale jeśli użyjemy skomentowanego kodu, otrzymamy różne wartości (tj. 1, 2, 3, 4). Czy ktokolwiek może wyjaśnić, dlaczego tak się dzieje?Problem z delegatami w C#

 delegate int Methodx(object obj); 

     static int DummyMethod(int i) 
     { 
      Console.WriteLine("In DummyMethod method i = " + i); 
      return i + 10; 
     } 

     static void Main(string[] args) 
     {  
      List<Methodx> methods = new List<Methodx>(); 

      for (int i = 0; i < 5; ++i) 
      { 
       methods.Add(delegate(object obj) { return DummyMethod(i); }); 
      } 

      //methods.Add(delegate(object obj) { return DummyMethod(1); }); 
      //methods.Add(delegate(object obj) { return DummyMethod(2); }); 
      //methods.Add(delegate(object obj) { return DummyMethod(3); }); 
      //methods.Add(delegate(object obj) { return DummyMethod(4); }); 

      foreach (var method in methods) 
      { 
       int c = method(null); 
       Console.WriteLine("In main method c = " + c); 
      } 
     } 

Również jeśli poniższy kod zostanie użyty, otrzymam pożądany wynik.

 for (int i = 0; i < 5; ++i) 
     { 
      int j = i; 
      methods.Add(delegate(object obj) { return DummyMethod(j); }); 
     } 
+2

Favouriting this question. Takie zachowanie jest po prostu "niebezpieczne". Założę się, że pojawią się one częściej jako jeden z tych trudnych do debugowania problemów, ponieważ C# porusza się coraz bardziej w kierunku zorientowanego na delegata sposobu zachowywania się w kapsule. Jest to podobne do zapętlonego odpowiednika klauzul przełączających, które mogą kaskadować w dół bez obowiązkowego "przerwania"; klauzula –

+0

@Neil: Zgadzam się, że to gorący punkt. Jest to bardziej zagmatwane, gdy jest postrzegane z "foreach" - tak bardzo, że zespół C# rozważa zmianę zachowania dla tej sprawy. (Trudno wyobrazić sobie scenariusz, w którym zachowanie foreach jest pożądane, sytuacja pętli for jest bardziej zrozumiała, ponieważ jasne jest, że zmienna jest zadeklarowana tylko raz.) –

+0

możliwy duplikat [C# przechwyconej zmiennej pętli] (http: //stackoverflow.com/questions/271440/c-sharp-captured-variable-in-loop) – nawfal

Odpowiedz

17

Problemem jest to, że jesteś uchwycenie tej samej zmiennej i w każdym delegata - który pod koniec pętli po prostu ma wartość 5.

Zamiast tego chce każdy delegat uchwycić inny zmienna, co oznacza deklarowania nowej zmiennej w pętli:

for (int i = 0; i < 5; ++i) 
{ 
    int localCopy = i; 
    methods.Add(delegate(object obj) { return DummyMethod(localCopy); }); 
} 

jest to dość powszechne „haczyka” - można przeczytać nieco więcej o przechwyconych zmiennych i zamknięć w my closures article.

+1

Jon, próbując zrozumieć, dlaczego tak się dzieje. Spojrzałem na "i" jako myślenie typu wartościowego "to musiało zostać przekazane jako kopia", więc nie mogłem zobaczyć, jak to wygląda, mając zachowanie podobne do referencji ... Masz podsumowanie/jedno-liniowe do wskazać mi właściwy kierunek? –

+2

Zobacz ostatni link. Najważniejsze, aby nie uchwycić wartości zmiennej *, która jest zmienną. –

+1

Pomyślcie o tym: To jest jak małe inline zdarzenie, które jest nazywane dokładnie w tym miejscu w metodzie, a zatem ma dostęp do wszystkich zmiennych i stanu, tylko otrzymujesz zmienne i określasz, kiedy wywoływana metoda nie została utworzona. W ten sposób po prostu uzyskujesz dostęp do i, gdy metoda ta nie jest wywoływana, gdy tworzysz anonimową metodę. – RCIX

2

Myślę, że to dlatego, że zmienna i kładzie się na stercie (jest to schwytany zmienna)

Spójrz na this answer.

+1

Myślę, że "zmienna przechwycona" jest bardziej użyteczną terminologią - właśnie dlatego, że przechwycono zmienną *, a nie jej * wartość *. –

+0

@Jon: Brzmi rozsądnie, naprawiam to –

4

Jeśli spojrzeć na kod generowany (przy użyciu reflektor) widać różnicę:

private static void Method2() 
{ 
    List<Methodx> list = new List<Methodx>(); 
    Methodx item = null; 
    <>c__DisplayClassa classa = new <>c__DisplayClassa(); 
    classa.i = 0; 
    while (classa.i < 5) 
    { 
     if (item == null) 
     { 
      item = new Methodx(classa.<Method2>b__8); 
     } 
     list.Add(item); 
     classa.i++; 
    } 
    foreach (Methodx methodx2 in list) 
    { 
     Console.WriteLine("In main method c = " + methodx2(null)); 
    } 
} 

Podczas korzystania początkowy kod to tworzy tymczasowy klasy w tle, ta klasa posiada odniesienie do Zmienna "i", tak jak na odpowiedź Jona, widzisz tylko ostateczną wartość tego.

private sealed class <>c__DisplayClassa 
{ 
    // Fields 
    public int i; 

    // Methods 
    public <>c__DisplayClassa(); 
    public int <Method2>b__8(object obj); 
} 

Naprawdę polecam patrząc na kod w Reflector aby zobaczyć, co się dzieje, jak się jej sens przechwyconych zmiennych. Upewnij się, że ustawiłeś Optymalizację kodu na ".NET 1.0" w menu Opcje, w przeciwnym razie ukryjesz wszystkie sceny za scenami.