Usar delegados en C #


79

En lenguaje C # y .NET framework, ¿podría ayudarme a comprender a los delegados? Estaba tratando de verificar algún código y descubrí que los resultados que recibí fueron inesperados para mí. Aquí está:

class Program
{
    public static int I = 0;

    static Func<string> del = new Func<string>(I.ToString);

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del());
    }
}

La respuesta fue 0, pero no 10. ¿Por qué?


12
@Rotem: No, no lo hizo.
Daniel Hilgarth

3
@Rotem: es una declaración de delegado. Agregar ()invocaría ToString.
Oded el

1
Lo siento, nunca usé Funcs, fue una suposición :)
Rotem

2
+1 para una buena pregunta, bien hecha. Gran ejemplo de cómo una pregunta aparentemente simple puede resaltar un área poco entendida del lenguaje / plataforma.
Martin

5
Una instancia de delegado (unidifusión) puede apuntar a un método de instancia o un staticmétodo. Cuando representa un método de instancia, el delegado contiene tanto el objeto "objetivo" en el que invocar el método como la información del método. Entonces, cuando diga del = I.ToString;, delmantendrá el objeto Ique aquí es un Int32(tipo de valor inmutable). Cuando usa una función anónima del = () => I.ToString();, el compilador crea un método static string xxx() { return I.ToString(); }y el delobjeto contiene ese método generado.
Jeppe Stig Nielsen

Respuestas:


79

La razón es la siguiente:

La forma en que declara el delegado apunta directamente al ToStringmétodo de la instancia int estática. Se captura en el momento de la creación.

Como señala flindeberg en los comentarios a continuación, cada delegado tiene un objetivo y un método para ejecutar en el objetivo.

En este caso, el método a ejecutar es obviamente el ToStringmétodo. La parte interesante es la instancia en la que se ejecuta el método: es la instancia de Ien el momento de la creación, lo que significa que el delegado no está usando Ipara que la instancia se use, sino que almacena la referencia a la instancia en sí.

Más tarde, cambia Ia un valor diferente, básicamente asignándole una nueva instancia. Esto no cambia mágicamente la instancia capturada en su delegado, ¿por qué debería hacerlo?

Para obtener el resultado que espera, deberá cambiar el delegado a esto:

static Func<string> del = new Func<string>(() => I.ToString());

Así, el delegado apunta a un método anónimo que se ejecuta ToStringen la corriente Ien el momento de la ejecución del delegado.

En este caso, el método a ejecutar es un método anónimo creado en la clase en la que se declara el delegado. La instancia es nula ya que es un método estático.

Eche un vistazo al código que genera el compilador para la segunda versión del delegado:

private static Func<string> del = new Func<string>(UserQuery.<.cctor>b__0);
private static string cctor>b__0()
{
    return UserQuery.I.ToString();
}

Como puede ver, es un método normal que hace algo . En nuestro caso, devuelve el resultado de llamar ToStringa la instancia actual de I.


1
@flindeberg: Incluso puedes usar tu propia clase en lugar de int. Seguirá comportándose igual, porque la razón subyacente no cambia: el delegado apunta a la encarnación específica de ToString en un objeto específico. No importa si se trata de un tipo de referencia o de un tipo de valor.
Daniel Hilgarth

3
@ user1859587: un delegado tiene un método y un objetivo (instancia), importa si captura su instancia o la instancia de la función lambda que, a su vez, contiene referencias a la instancia.
flindeberg

1
@ user1859587: De nada. Por cierto: traté de actualizar la respuesta para aclarar un poco lo que está sucediendo aquí. Es posible que desee volver a leerlo :-)
Daniel Hilgarth

3
Daniel, solo para confirmar el comentario de flindeberg: tu respuesta es correcta pero tus comentarios sobre el boxeo no lo son. user1859587 es correcto: el comportamiento observado es una consecuencia del hecho de que el delegado captura al receptor de la llamada. Aunque el receptor de una llamada a ToString en int sería una referencia a una variable int, el delegado no tiene forma de poner una referencia a una variable int en el montón; las referencias a variables solo pueden almacenarse temporalmente. Entonces, hace la siguiente mejor opción: recubre el int y hace una referencia a esa ubicación del montón.
Eric Lippert

10
Una consecuencia interesante del hecho de que el receptor está encajonado es que es imposible hacer un delegado a GetValueOrDefault () en un int que acepta valores NULL, porque encajonar un int que acepta valores NULL produce un int encajonado, no un int encajonado que acepta valores nulos, y un int encajado tiene sin método GetValueOrDefault ().
Eric Lippert

4

Debe pasar Ia su función para que I.ToString()pueda ejecutarse en el momento adecuado (en lugar de en el momento en que se crea la función).

class Program
{
    public static int I = 0;

    static Func<int, string> del = num => num.ToString();

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del(I));
    }
}

1

Así es como se debe hacer esto:

using System;

namespace ConsoleApplication1
{

    class Program
    {
        public static int I = 0;

        static Func<string> del = new Func<string>(() => {
            return I.ToString();
        });

        static void Main(string[] args)
        {
            I = 10;
            Console.WriteLine("{0}", del());
        }
    }
}


-2

Supongo que los valores int se pasan por valores, no por referencias, y por esa razón al crear el delegado, es un delegado al método ToString del valor actual de "I" (0).


2
Tu conjetura no es correcta. Esto no tiene nada que ver con los tipos de valor frente a los tipos de referencia. Lo mismo sucedería exactamente con los tipos de referencia.
Daniel Hilgarth

En realidad lo es, si por ejemplo usamos una instancia de clase y ToString estaba manipulando los datos de la instancia para el valor de retorno, produciría el valor de retorno del estado de la clase actual, no el estado de la clase cuando se creó el delegado. La función no se ejecuta cuando se crea el delegado y solo hay una instancia de la clase.
Yshayy

Func <cadena> (() => I.ToString ()) Debería funcionar también porque no usaremos "I" hasta la invocación del método.
Yshayy

Pero eso no es equivalente a lo que está sucediendo aquí. Si usara la clase en Foolugar de inty cambiara la línea I = 10a I = new Foo(10), tendría exactamente el mismo resultado que con el código actual. I.Value = 10es algo completamente diferente. Esto no asigna una nueva instancia a I. Pero asignar una nueva instancia a Ies el punto importante aquí.
Daniel Hilgarth

2
ok, entonces el problema es reasignar "I", si "I" fuera mutable y cambiamos el objeto sin reasignar I, habría funcionado. en este ejemplo, no podemos hacerlo porque I es int (inmutable).
Yshayy
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.