En C # 7.0 puede declarar funciones locales, es decir, funciones que viven dentro de otro método. Estas funciones locales pueden acceder a variables locales del método circundante. Dado que las variables locales existen solo mientras se llama a un método, me preguntaba si se podría asignar una función local a un delegado (que puede vivir más que esta llamada al método).

public static Func<int,int> AssignLocalFunctionToDelegate()
{
    int factor;

    // Local function
    int Triple(int x) => factor * x;

    factor = 3;
    return Triple;
}

public static void CallTriple()
{
    var func = AssignLocalFunctionToDelegate();
    int result = func(10);
    Console.WriteLine(result); // ==> 30
}

¡De hecho funciona!

Mi pregunta es: ¿por qué funciona esto? ¿Que esta pasando aqui?

13
Olivier Jacot-Descombes 13 dic. 2016 a las 23:16

2 respuestas

La mejor respuesta

Esto funciona porque el compilador crea un delegado que captura la variable factor en un cierre.

De hecho, si usa un descompilador, verá que se genera el siguiente código:

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    int factor = 3;
    return delegate (int x) {
        return (factor * x);
    };
}

Puede ver que factor será capturado en un cierre. (Probablemente ya sepa que detrás de escena el compilador generará una clase que contiene un campo para contener factor).

En mi máquina, crea la siguiente clase para actuar como cierre:

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
    // Fields
    public int factor;

    // Methods
    internal int <AssignLocalFunctionToDelegate>g__Triple0(int x)
    {
        return (this.factor * x);
    }
}

Si cambio AssignLocalFunctionToDelegate() a

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    int factor;
    int Triple(int x) => factor * x;
    factor = 3;
    Console.WriteLine(Triple(2));
    return Triple;
}

Entonces la implementación se convierte en:

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    <>c__DisplayClass1_0 CS$<>8__locals0;
    int factor = 3;
    Console.WriteLine(CS$<>8__locals0.<AssignLocalFunctionToDelegate>g__Triple0(2));
    return delegate (int x) {
        return (factor * x);
    };
}

Puede ver que está creando una instancia de la clase generada por el compilador para usar con Console.WriteLine ().

Lo que no puede ver es dónde realmente asigna 3 a factor en el código descompilado. Para ver eso, debe mirar el IL en sí (esto puede ser un error en el descompilador que estoy usando, que es bastante antiguo).

El IL se ve así:

L_0009: ldc.i4.3 
L_000a: stfld int32 ConsoleApp3.Program/<>c__DisplayClass1_0::factor

Eso es cargar un valor constante de 3 y almacenarlo en el campo factor de la clase de cierre generada por el compilador.

8
Matthew Watson 13 dic. 2016 a las 21:18

Dado que las variables locales existen solo mientras se llama a un método,

Esta afirmación es falsa. Y una vez que crea una declaración falsa, toda su cadena de razonamiento ya no es sólida.

"La duración no es superior a la activación del método" no es una característica definitoria de las variables locales. La característica definitoria de una variable local es que el nombre de la variable solo es significativo para codificar en el ámbito local de la variable .

¡No combine el alcance con la vida útil! No són la misma cosa. La vida útil es un concepto de tiempo de ejecución que describe cómo se recupera el almacenamiento. El alcance es un concepto en tiempo de compilación que describe cómo se asocian los nombres con los elementos del lenguaje. Las variables locales se denominan locales debido a su alcance local; su localidad tiene que ver con sus nombres, no con sus vidas.

Las variables locales pueden tener su vida útil extendida o acortada arbitrariamente por razones de desempeño o corrección. No hay ningún requisito en C # de que las variables locales solo tengan una vida útil mientras el método está activado.

Pero tu ya lo sabías:

IEnumerable<int> Numbers(int n)
{
  for (int i = 0; i < n; i += 1) yield return i;
}
...
var nums = Numbers(7);
foreach(var num in nums)
  Console.WriteLine(num);

Si la vida útil de los locales iyn se limita al método, entonces, ¿cómo pueden iyn seguir teniendo valores después de que Numbers regrese?

Task<int> FooAsync(int n)
{
  int sum = 0;
  for(int i = 0; i < n; i += 1)
    sum += await BarAsync(i);
  return sum;
}
...
var task = FooAsync(7);

FooAsync devuelve una tarea después de la primera llamada a BarAsync. Pero de alguna manera sum y n y i siguen teniendo valores, incluso después de que FooAsync regresa al llamador.

Func<int, int> MakeAdder(int n)
{
  return x => x + n;
}
...
var add10 = MakeAdder(10);
Console.WriteLine(add10(20));

De alguna manera n se queda incluso después de que MakeAdder regresara.

Las variables locales pueden sobrevivir fácilmente después de que regrese el método que las activó; esto sucede todo el tiempo en C #.

¿Qué está pasando aquí?

Una función local convertida en un delegado es lógicamente no muy diferente a una lambda; ya que podemos convertir lambdas en delegados, también podemos convertir métodos locales en delegados.

Otra forma de pensarlo: suponga que su código fuera:

return y=>Triple(y);

Si no ve ningún problema con esa lambda, entonces no debería haber ningún problema simplemente con return Triple;; nuevamente, esos dos fragmentos de código son lógicamente la misma operación, por lo que si hay una estrategia de implementación para uno, luego hay una estrategia de implementación para el otro.

Tenga en cuenta que lo anterior no pretende implicar que el equipo del compilador sea requerido para generar métodos locales como lambdas con nombres. El equipo del compilador es, como siempre, libre de elegir la estrategia de implementación que desee, dependiendo de cómo se utilice el método local. Así como el equipo del compilador tiene muchas variaciones menores en la estrategia para generar una conversión de lambda a delegado dependiendo de los detalles de la lambda.

Si, por ejemplo, le preocupan las implicaciones de rendimiento de estas diversas estrategias, entonces, como siempre, no hay sustituto para probar escenarios realistas y obtener mediciones empíricas.

20
Eric Lippert 24 may. 2018 a las 20:56