2011-12-13 5 views
5

Mi sono imbattuto in quello che per me è stato un risultato inatteso durante il test di un semplice metodo di estensione ForEach.È possibile che un'azione/un delegato modifichi il suo valore degli argomenti?

ForEach metodo

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 metodo

[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()); 
} 

Perché sarebbe numbers.Sum() essere uguale a 55 e non 0?

risposta

5

num è la copia del valore dell'elemento corrente su cui si sta iterando. Quindi stai solo cambiando la copia.

quello che fai è fondamentalmente questo:

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

Sicuramente non aspettatevi questo per cambiare il contenuto della matrice?

Edit: Quello che vuoi è questo:

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

Nel vostro caso specifico si potrebbe mantenere un indice nel vostro metodo di estensione ForEach e passare che come secondo argomento all'azione e quindi utilizzarlo come questo :

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

Tuttavia, in generale: Creazione di Linq metodi di estensione stile che modificano la raccolta vengono applicati a sono cattivo stile (IMO). Se scrivi un metodo di estensione che non può essere applicato a un IEnumerable<T>, dovresti davvero pensarci seriamente se ne hai davvero bisogno (specialmente quando scrivi con l'intenzione di modificare la raccolta). Non hai molto da guadagnare ma molto da perdere (come effetti collaterali inaspettati). Sono sicuro che ci sono delle eccezioni, ma aderisco a quella regola e mi è servita bene.

+0

@ 249076: Sì, funzionerebbe. –

+0

Sono d'accordo quello che voglio è un ciclo for, ma come faccio a farlo funzionare in un metodo di estensione se l'argomento è passato per valore quando viene effettuata la chiamata all'azione? Non sembra che ci sia un modo per passare il valore facendo riferimento all'azione delegato che viene passata alla funzione ForEach. Immagino che tutti lo sappiano, ma non mi sono reso conto che foreach (int num in numbers) {num = 0; } non funzionerebbe Perché Num è una copia temporanea e non un riferimento? Pensavo che foreach fosse solo zucchero sintattico per "for". Credo di aver bisogno di smettere di fare tante ipotesi. – 249076

+0

Mi piacerebbe capire un modo per scrivere l'estensione ForEach in modo che possa modificare il valore di un int. Non credo che abbia senso, ma è un enigma che vorrei risolvere. – 249076

0

Perché int è un value type e viene passato al metodo di estensione come parametro di valore. Pertanto una copia di numbers viene passata al metodo ForEach. I valori memorizzati nell'array numbers inizializzato nel metodo BasicForEachTest non vengono mai modificati.

Controllare questo article da Jon Skeet per saperne di più su tipi di valore e parametri di valore.

1

Perché num è una copia. E 'come se si stesse facendo questo:

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

non ci si aspetterebbe che per cambiare i numeri [0], vero?

0

Non sto affermando che il codice in questa risposta sia utile, ma (funziona e) Penso che illustri ciò di cui hai bisogno per far funzionare il tuo approccio. L'argomento deve essere contrassegnato con ref.Il BCL non ha un tipo di delegato con ref, quindi basta scrivere il proprio (non all'interno di ogni classe):

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

Con questo, il metodo diventa:

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]); 
    } 
} 

Ora, ricordate di usare il ref parola chiave nel vostro metodo di prova:

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

Questo funziona perché è OK per passare una voce matrice ByRef (ref).

Se si desidera estendere IList<>, invece, quello che dovete fare:

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; 
    } 
} 

Spero che questo aiuti la vostra comprensione.

Nota: ho dovuto utilizzare i loop for. In C#, in foreach (var x in Yyyy) { /* ... */ }, non è consentito assegnare a x (che include il passaggio di x ByRef (con ref o out)) all'interno del corpo del ciclo.