2010-11-21 41 views
39

Estoy intentando crear una función que pueda crear una acción que incremente el entero que se pasa. Sin embargo, mi primer intento me da un error "no puedo usar ref o parámetro dentro de un cuerpo de método anónimo ".C# No se puede utilizar el parámetro ref o out dentro de un método anónimo body

public static class IntEx { 
    public static Action CreateIncrementer(ref int reference) { 
     return() => { 
      reference += 1; 
     }; 
    } 
} 

entiendo por qué el compilador no le gusta esto, pero no obstante me gustaría tener una forma elegante para proporcionar una fábrica incrementador agradable que puede apuntar a cualquier número entero. La única manera de que estoy viendo de hacer esto es algo como lo siguiente:

public static class IntEx { 
    public static Action CreateIncrementer(Func<int> getter, Action<int> setter) { 
     return() => setter(getter() + 1); 
    } 
} 

Pero por supuesto que es más de un dolor de la persona que llama a utilizar; requiriendo que la persona que llama cree dos lambdas en lugar de solo pasar una referencia. ¿Hay alguna forma más elegante de proporcionar esta funcionalidad, o tendré que vivir con la opción de dos lambda?

+2

¿Es este un ejemplo simplificado? ¿Por qué no usar x ++? ¿Por qué hay otro estado de incremento de clase de esta clase? – Gishu

+2

@Gishu Sí, este es un ejemplo simplificado; el caso de uso más grande es más difícil de explicar, pero todo se reduce a la creación de una fábrica de Acción que puede realizar operaciones en tipos de valores. –

Respuesta

24

Esto no es posible.

El compilador transformará todas las variables locales y los parámetros utilizados por métodos anónimos en campos de una clase de cierre generada automáticamente.

El CLR no permite almacenar los tipos ref en los campos.

Por ejemplo, si pasa un tipo de valor en una variable local como un parámetro ref, la vida útil del valor se extendería más allá de su marco de pila.

30

Está bien, he encontrado que en realidad es es posible con punteros si en su contexto inseguro:

public static class IntEx { 
    unsafe public static Action CreateIncrementer(int* reference) { 
     return() => { 
      *reference += 1; 
     }; 
    } 
} 

Sin embargo, el recolector de basura puede causar estragos con esta moviendo su referencia durante la recogida de basuras, según se a continuación se indica:

class Program { 
    static void Main() { 
     new Program().Run(); 
     Console.ReadLine(); 
    } 

    int _i = 0; 
    public unsafe void Run() { 
     Action incr; 
     fixed (int* p_i = &_i) { 
      incr = IntEx.CreateIncrementer(p_i); 
     } 
     incr(); 
     Console.WriteLine(_i); // Yay, incremented to 1! 
     GC.Collect(); 
     incr(); 
     Console.WriteLine(_i); // Uh-oh, still 1! 
    } 
} 

Uno puede solucionar este problema fijando la variable a un punto específico en la memoria. Esto se puede hacer mediante la adición del siguiente al constructor:

public Program() { 
     GCHandle.Alloc(_i, GCHandleType.Pinned); 
    } 

que mantiene el recolector de basura se mueva el objeto alrededor, por lo que es exactamente lo que estamos buscando. Sin embargo, debes agregar un destructor para liberar el pin, y fragmenta la memoria durante toda la vida útil del objeto. No es realmente más fácil. Esto tendría más sentido en C++, donde las cosas no se mueven, y la gestión de recursos es el mismo, pero no tanto en C# donde todo lo que se supone es automático.

Parece que la moraleja de la historia es, simplemente envuelva ese miembro int en un tipo de referencia y termine con él.

(Y sí, esa es la forma en que había que trabajar antes de hacer la pregunta, pero sólo estaba tratando de averiguar si había una manera de que pudiera deshacerse de toda mi referencia < variables miembro int > y sólo tiene que utilizar enteros normales Oh, bueno.)

+4

+1 para una solución interesante del modo administrado que usa contexto inseguro. –

+0

No hay necesidad de GCHandle.Alloc: simplemente extienda las llaves corredizas de estado fijo. –

+0

@ Mr.TA Ese fue solo un ejemplo para demostrar que el GC podría estropear las cosas. Cualquier uso valioso de la función 'incr' probablemente lo devolvería desde el método que lo crea, o lo agregará a un objeto persistente como una variable miembro, que obviamente deja el alcance" fijo ". –

2

Podría haber sido una característica útil para el tiempo de ejecución permitir la creación de referencias de variables con un mecanismo para evitar su persistencia; dicha característica habría permitido que un indexador se comportara como una matriz (por ejemplo, se podía acceder a un Dictionary < Int32, Point> a través de "myDictionary [5] .X = 9;").Creo que tal característica podría haber sido proporcionada de manera segura si tales referencias no pudieran ser downcast a otros tipos de objetos, ni usadas como campos, ni pasadas por referencia (ya que cualquier lugar donde tal referencia pueda ser almacenada saldría del alcance antes de la referencia sí mismo lo haría). Desafortunadamente, el CLR no proporciona esa característica.

Para implementar lo que necesita, el llamador de cualquier función que utilice un parámetro de referencia dentro de un cierre debe envolver dentro de un cierre cualquier variable que desee pasar a dicha función. Si hubiera una declaración especial para indicar que un parámetro se usaría de esa manera, podría ser práctico para un compilador implementar el comportamiento requerido. Tal vez en un compilador .NET 5.0, aunque no estoy seguro de lo útil que sería.

Por cierto, entiendo que los cierres en Java usan semántica por valor de valor, mientras que los de .net son de referencia. Puedo entender algunos usos ocasionales de la semántica de referencia por referencia, pero el uso de la referencia por defecto parece una decisión dudosa, análoga al uso de la semántica de paso de parámetros por referencia por defecto para las versiones de VB hasta VB6. Si uno quiere capturar el valor de una variable al crear un delegado para llamar a una función (por ejemplo, si uno quiere que un delegado llame a MyFunction (X) usando el valor de X cuando se crea el delegado), ¿es mejor usar un lambda? con una temperatura extra, o es mejor simplemente usar una fábrica de delegados y no molestarse con las expresiones de Lambda.

+1

Sí, Eric Lippert escribió un blog sobre eso. http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx. De hecho, tengo un código que depende de este comportamiento; manteniendo un conteo de cuadros para el bucle OpenGL sin necesidad de una variable miembro de marco, por ejemplo. Me gusta la especificación C++ mejor que permite semántica. –

+1

@Dax: hay casos en que la semántica de paso por referencia es necesaria, sin duda. Lo mismo es cierto, sin embargo, del paso de parámetros. Eso no implica que pass-by-reference sea el valor predeterminado. Por cierto, me pregunto cuáles serían los pros y los contras de reemplazar las variables de cierre por referencia con matrices de un solo elemento, permitiendo crear diferentes clases para métodos anónimos que necesitan diferentes combinaciones de variables de cierre (evitando algunos problemas de GC). – supercat

Cuestiones relacionadas