2011-12-13 17 views
5

Wpadłem na coś, co było dla mnie nieoczekiwanym rezultatem podczas testowania prostej metody rozszerzenia ForEach.Czy akcja/delegat może zmienić wartość argumentów?

ForEach metoda

public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) 
{ 
    if (action == null) throw new ArgumentNullException("action"); 

    foreach (T element in list) 
    { 
     action(element); 
    } 
} 

Test metoda

[TestMethod] 
public void BasicForEachTest() 
{ 
    int[] numbers = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 

    numbers.ForEach(num => 
    { 
     num = 0; 
    }); 

    Assert.AreEqual(0, numbers.Sum()); 
} 

Dlaczego numbers.Sum() być równa 55 a nie 0?

Odpowiedz

5

num jest kopią wartości bieżącego elementu, który iterujesz. Więc właśnie zmieniasz kopię.

Co robisz, jest to zasadniczo:

foreach(int num in numbers) 
{ 
    num = 0; 
} 

pewno nie spodziewałem się tego, aby zmienić zawartość tablicy?

Edit: Co chcesz to:

for (int i in numbers.Length) 
{ 
    numbers[i] = 0; 
} 

W Twoim konkretnym przypadku można utrzymać indeks w ForEach metodę rozszerzenia i przekazać, że jako drugi argument do działania, a następnie wykorzystać je w ten sposób :

numbers.ForEachWithIndex((num, index) => numbers[index] = 0); 

jednak ogólnie: Tworzenie Linq metod rozszerzeń, które modyfikują styl kolekcji są one stosowane do stylu są złe (IMO). Jeśli piszesz metodę rozszerzenia, której nie można zastosować do IEnumerable<T>, powinieneś naprawdę się nad tym zastanowić, jeśli naprawdę jej potrzebujesz (szczególnie gdy piszesz z zamiarem modyfikacji kolekcji). Nie masz wiele do zyskania, ale wiele do stracenia (jak niespodziewane efekty uboczne). Jestem pewien, że są wyjątki, ale trzymam się tej zasady i dobrze mi to służyło.

+0

@ 249076: Tak, to by działało. –

+0

Zgadzam się, co chcę, aby była pętlą for, ale w jaki sposób sprawiłbym, żeby działała w metodzie rozszerzenia, jeśli argument zostanie przekazany przez wartość, gdy zostanie wykonane wezwanie do działania? Nie wygląda na to, że istnieje sposób przekazania wartości przez odniesienie do delegata akcji, który jest przekazywany do funkcji ForEach. Sądzę, że wszyscy o tym wiedzą, ale nie zdawałem sobie sprawy, że foreach (int num in numbers) {num = 0; } nie działa. Dlaczego num jest kopią tymczasową, a nie referencją? W pewnym sensie założyłem, że foreach był po prostu cukrem syntaktycznym dla "za". Chyba muszę przestać robić tak wiele założeń. – 249076

+0

Chciałbym znaleźć sposób, aby napisać rozszerzenie ForEach, aby mógł zmienić wartość int. Nie sądzę, żeby to miało sens, ale jest to zagadka, którą chciałbym rozwiązać. – 249076

0

Ponieważ int to value type i jest przekazywana do metody rozszerzenia jako parametr wartości. Dlatego kopia numbers jest przekazywana do twojej metody ForEach. Wartości przechowywane w macierzy numbers, która jest inicjowana w metodzie BasicForEachTest, nigdy nie są modyfikowane.

Sprawdź ten article przez Jona Skeeta, aby przeczytać więcej o typach wartości i parametrach wartości.

1

Ponieważ num jest kopią. To tak, jakby w ten sposób:

int i = numbers[0]; 
i = 0; 

Nie spodziewałbym się, że aby zmienić numery [0], prawda?

0

Nie twierdzę, że kod w tej odpowiedzi jest przydatny, ale (działa i) Myślę, że ilustruje to, czego potrzebujesz, aby Twoje podejście zadziałało. Argument musi być oznaczony jako ref.Plc nie posiada typ delegata z ref, więc po prostu napisz własną rękę (nie wewnątrz każdej klasy):

public delegate void MyActionRef<T>(ref T arg); 

z tym, metoda staje:

public static void ForEach2<T>(this T[] list, MyActionRef<T> actionRef) 
{ 
    if (actionRef == null) 
    throw new ArgumentNullException("actionRef"); 

    for (int idx = 0; idx < list.Length; idx++) 
    { 
    actionRef(ref list[idx]); 
    } 
} 

Teraz należy pamiętać, aby używać ref kluczowe w metodzie badania:

numbers.ForEach2((ref int num) => 
{ 
    num = 0; 
}); 

działa to dlatego, że jest OK, aby przejść wpisu tablicy ByRef (ref).

Jeśli chcesz przedłużyć IList<> zamiast tego trzeba zrobić:

public static void ForEach3<T>(this IList<T> list, MyActionRef<T> actionRef) 
{ 
    if (actionRef == null) 
    throw new ArgumentNullException("actionRef"); 

    for (int idx = 0; idx < list.Count; idx++) 
    { 
    var temp = list[idx]; 
    actionRef(ref temp); 
    list[idx] = temp; 
    } 
} 

Nadzieja to pomaga zrozumienie.

Uwaga: Musiałem użyć pętli for. W języku C#, w foreach (var x in Yyyy) { /* ... */ }, nie można przypisać do x (która obejmuje przekazywanie x ByRef (z ref lub out)) wewnątrz ciała pętli.