2011-12-13 11 views
5

Me encontré con lo que era para mí un resultado inesperado al probar un método de extensión simple ForEach.¿Puede una acción/delegar cambiar el valor de sus argumentos?

ForEach método

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 método

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

¿Por qué numbers.Sum() ser igual a 55 y no es 0?

Respuesta

5

num es la copia del valor del elemento actual sobre el que está iterando. Entonces solo estás cambiando la copia.

Lo que se hace es básicamente el siguiente:

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

Seguro que no esperamos que esto cambie el contenido de la matriz?

Editar: Lo que se quiere es la siguiente:

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

En su caso específico se podía mantener un índice en el método de extensión ForEach y pasar que, como segundo argumento de la acción y luego usarlo como esto :

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

Sin embargo, en general: Creación de Linq métodos de extensión que modifican el estilo de la colección que se aplican al estilo son malos (OMI). Si escribe un método de extensión que no se puede aplicar a un IEnumerable<T>, debería pensarlo realmente si realmente lo necesita (especialmente cuando escribe con la intención de modificar la colección). No tiene mucho que ganar, sino mucho que perder (como efectos secundarios inesperados). Estoy seguro de que hay excepciones, pero me atengo a esa regla y me ha servido bien.

+0

@ 249076: Sí, esto funcionaría. –

+0

Acepto que lo que quiero es un bucle for, pero ¿cómo lo haría funcionar en un método de extensión si el argumento se pasa por valor cuando se realiza la llamada a la acción? No parece que haya una forma de pasar el valor por referencia al delegado de acción que se pasa a la función ForEach. Supongo que todos lo saben, pero no me di cuenta de que foreach (int num en números) {num = 0; } no funcionaría. ¿Por qué num es una copia temporal y no una referencia? Supuse que foreach era solo azúcar sintáctico para "para". Creo que necesito dejar de hacer tantas suposiciones. – 249076

+0

Me gustaría encontrar una forma de escribir esa extensión ForEach para que pueda cambiar el valor de un int. No creo que tenga sentido, pero es un rompecabezas que me gustaría resolver. – 249076

0

Porque int es value type y se pasa a su método de extensión como un parámetro de valor. Por lo tanto, se pasa una copia de numbers a su método ForEach. Los valores almacenados en la matriz numbers que se inicializa en el método BasicForEachTest nunca se modifican.

Compruebe esto article por Jon Skeet para leer más sobre los tipos de valor y los parámetros de valor.

1

Porque num es una copia. Es como si estuviera haciendo esto:

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

no es de esperar que para cambiar los números [0], ¿verdad?

0

No estoy diciendo que el código de esta respuesta sea útil, pero (funciona y) creo que ilustra lo que necesita para que su enfoque funcione. El argumento debe marcarse ref.El BCL no tiene un tipo de delegado con ref, por lo que acaba de escribir su propio (no dentro de cualquier clase):

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

Con esto, el método se convierte en:

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

Ahora, recuerde utilizar el ref palabra clave en el método de prueba:

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

Esto funciona porque está bien para pasar una entrada de la matriz ByRef (ref).

Si desea ampliar IList<> lugar, usted tiene que hacer:

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

Esperanza esto ayuda a su comprensión.

Nota: tuve que usar for bucles. En C#, en foreach (var x in Yyyy) { /* ... */ }, no está permitido asignar a x (que incluye pasar x ByRef (con ref o out)) dentro del cuerpo del bucle.

Cuestiones relacionadas