2017-01-12 43 views
5

Mam następujący -Stosując metody anonimowe wewnątrz opóźnionego zadania wewnątrz pętli

for (int i = 0; i < N; ++i) 
{ 
    var firstTask = DoSomething(...); 

    var task = anotherTask.ContinueWith(
     (t) => ProcessResult(i, t.Result)); 
} 

Problem jest, że wartość i przeszedł do ProcessResult wydaje się być wartość, kiedy zaczyna, nie wartość iteracja po jej utworzeniu.

Jaki jest najlepszy sposób ochrony przed tym?

+0

Dlaczego używasz 'ContinueWith' zamiast czekać? Jeśli chodzi o 'i' twoja lambda przechwytuje zmienną *, a nie wartość zmiennej. Czytanie 'i' zwróci to, co" i "zawiera, kiedy faktycznie je czytasz - kiedy wywołanie funkcji' ProcessResult (i, ..) 'zostanie faktycznie wykonane. Jest to oczekiwane zachowanie przy okazji. Użycie 'await' naprawi to przez pozbycie się lambda * i * uprościć kod –

+0

Aby połączyć zadania. Mogą one być długotrwałe, a oczekują zawieszenia bieżącego wątku. – Hector

+0

Nie, nie będzie. 'Czekaj' * czeka *, nie blokuje.Jest to odpowiednik 'ContinueWith', a nie' Wait'. Sprawia, że ​​tworzenie łańcucha * jest łatwiejsze *, ponieważ nie wymaga lambd i przechwytywania. –

Odpowiedz

2

Musisz przechwycić wartość i w swojej własnej zmiennej.

for (int i = 0; i < N; ++i) 
{ 
    var count = i; 

    var firstTask = DoSomething(...); 

    var task = anotherTask.ContinueWith(
     (t) => ProcessResult(count, t.Result)); 
} 

Przykład:

for (int i = 0; i < 5; ++i) 
{ 
    var a = i; 
    var task = Task.Delay(0).ContinueWith((t) => a.Dump()); 
} 

wyprowadza To coś jak:

0 
2 
1 
4 
3 

Ale:

for (int i = 0; i < 5; ++i) 
{ 
    var task = Task.Delay(0).ContinueWith((t) => i.Dump()); 
} 

Wyjścia:

5 
5 
5 
5 
5 
2

Musisz utworzyć zmienną temp wewnątrz pętli; w bieżącym kodzie przechwytywana jest zmienna , która nie jest wartością, co oznacza, że ​​po ostatecznym wykonaniu zadań kontynuacji pętla została już zakończona, a i jest N-1.

for (int i = ...) 
{ 
    var temp = i; 
    var task = anotherTask.ContinueWith(t => ProcessResult(temp, t.Resume)); 
} 
+1

Lub użyj 'oczekuj' zamiast' ContinueWith' –

+1

@PanagiotisKanavos tak, ale pytanie brzmi, dlaczego kod zachowuje się tak, jak robi, a nie, jeśli jest to najlepszy sposób na wykonanie tej pracy. – InBetween

+0

Dzięki! Rozumiem, że kompilator/garbage collector może obsłużyć pętlę w ruchu, a zmienna nie zostanie usunięta? – Hector

2

Wartość lambda, która wykorzystuje zmienną zewnętrzną, przechwytuje zmienną, a nie zapisaną w niej wartość. Oznacza to, że w miarę postępu w pętli zmienia się również wartość, którą można odczytać z przechwyconej zmiennej.

Można to naprawić, używając zmiennej tymczasowej w pętli. Twój kod będzie dużo czystsze chociaż jeśli użyto async/await zamiast ContinueWith i lambda, np:

for (int i=0;i<N;i++) 
{ 
    //... 
    var result=await thatOtherAsyncMethod(...); 
    ProcessResult(i, result)); 
} 

Ogólnie, można uniknąć problemów przechwytywania kopiując zmiennej pętli do zmiennej zdefiniowanej wewnątrz zakresu pętli za.

Rozwiązuje to problem, ponieważ zmienna tymczasowa istnieje tylko w ciele pętli. Lambda jest również tworzone wewnątrz ciała pętli i oddaje lokalną, niezmienny zmiennej:

for (int i=0;i<N;i++) 
{ 
    var temp=i; 
    var myLambda = new Action(()=>MyMethod(temp)); 

    //This runs with the local copy, not i 
    myLambda(); 
} 

Jeszcze lepszym sposobem jest jednak do unikać przechwytuje i przekazać wartość pętli jako parametr państwowej do ContinueWith, np .:

for (int i = 0; i < N; ++i) 
{ 
    //... 
    var task = anotherTask.ContinueWith(
           (t,state) => ProcessResult((int)state, t.Result), 
           i); 
    //... 
}