2010-02-16 32 views
7

Tengo una simple pregunta sobre los delegados de .net. Decir que tengo algo como esto:C# delegados, tiempo de resolución de referencia

public void Invoke(Action<T> action) 
    { 
     Invoke(() => action(this.Value)); 
    } 

    public void Invoke(Action action) 
    { 
     m_TaskQueue.Enqueue(action); 
    } 

La primera función encierra una referencia a this.Value. Durante el tiempo de ejecución, cuando se llama al primer método con parámetro genérico, proporcionará this.Value de alguna manera al segundo, pero ¿cómo? Éstos vinieron a la mente:

  • llamada por valor (struct) - el valor actual de this.Value se pasa, por lo que si el m_TaskQueue lo ejecuta 5 minutos más tarde, el valor no será en su estado reciente, será lo que sea cuando se hace referencia por primera vez.
  • la llamada por referencia (tipo de referencia) - entonces el más reciente estado de Value se hará referencia durante la ejecución de la acción, pero si cambio this.Value a otra referencia antes de la ejecución de la acción, todavía estará apuntando a la edad de referencia
  • Llamar por nombre (ambos) - donde this.Value se evaluará cuando se llame a la acción. Creo que la implementación real tendría una referencia al this y luego se evaluará Value durante la ejecución real del delegado, ya que no hay una llamada por nombre.

Supongo que sería un estilo Call of name pero no pudo encontrar ninguna documentación, por lo que me pregunto si se trata de un comportamiento bien definido. Esta clase es algo así como un Actor en Scala o Erlang, así que necesito que sea seguro para subprocesos. No deseo que la función Invoke desreferencia Value inmediatamente, que se hará en un hilo seguro para el objeto this por m_TaskQueue.

Respuesta

18

Déjeme responder su pregunta describiendo qué código generamos realmente para esto. Cambiaré el nombre del otro método invocado de manera confusa; no es necesario entender lo que está pasando aquí.

Supongamos que usted ha dicho

class C<T> 
{ 
    public T Value; 
    public void Invoke(Action<T> action) 
    { 
     Frob(() => action(this.Value)); 
    } 
    public void Frob(Action action) 
    { // whatever 
    } 
} 

El compilador genera código como si realmente hubiera escrito:

class C<T> 
{ 
    public T Value; 

    private class CLOSURE 
    { 
    public Action<T> ACTION; 
    public C<T> THIS; 
    public void METHOD() 
    { 
     this.ACTION(this.THIS.Value); 
    } 
    } 

    public void Invoke(Action<T> action) 
    { 
     CLOSURE closure = new CLOSURE(); 
     closure.THIS = this; 
     closure.ACTION = action; 
     Frob(new Action(closure.METHOD)); 
    } 
    public void Frob(Action action) 
    { // whatever 
    } 
} 

¿Eso responde a su pregunta?

+0

Muchas gracias, es claro como el cristal ahora :) –

7

El delegado almacena una referencia a la variable, no el valor de la misma. Si desea mantener el valor actual a continuación (suponiendo que es un tipo de valor) que necesita para hacer una copia local de la misma:

public void Invoke(Action<T> action) 
{ 
    var localValue = this.Value; 
    Invoke(() => action(localValue)); 
} 

Si se trata de un tipo de referencia mutable se podía hacer un clon locales/copia profunda .

+1

obligatorio Eric Lippert plug: http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx –

3

La verdadera clave es recordar que el alcance es léxico; es algo de lo que se ocupa el compilador. Por lo tanto, captura variables, no sus valores . Si esos valores son tipos de valores o tipos de referencia es otro asunto completamente diferente.

Tal vez un ejemplo un poco más extremo de alterar el comportamiento del delegado ayudará:

var myVariable = "something"; 
Action a =() => Console.WriteLine(myVariable); 
myVariable = "something else entirely" 
a(); 

impresiones "algo completamente distinto". En esa luz, en realidad no importa cuántas veces envuelva, guarde o mueva la función; todavía se refiere a la variable que encerró. Entonces, en resumen, lo que importa es el valor de la variable adjunta cuando el delegado realmente se ejecuta.