La respuesta de Eric Lippert realmente llega al punto. Sin embargo, sería bueno construir una imagen de cómo funcionan los marcos de pila y las capturas en general. Hacer esto ayuda a mirar un ejemplo un poco más complejo.
Aquí es el código de captura:
public class Scorekeeper {
int swish = 7;
public Action Counter(int start)
{
int count = 0;
Action counter =() => { count += start + swish; }
return counter;
}
}
Y aquí es lo que yo creo que el equivalente sería (si tenemos suerte Eric Lippert comentarios sobre si esto es realmente correcto o no):
private class Locals
{
public Locals(Scorekeeper sk, int st)
{
this.scorekeeper = sk;
this.start = st;
}
private Scorekeeper scorekeeper;
private int start;
public int count;
public void Anonymous()
{
this.count += start + scorekeeper.swish;
}
}
public class Scorekeeper {
int swish = 7;
public Action Counter(int start)
{
Locals locals = new Locals(this, start);
locals.count = 0;
Action counter = new Action(locals.Anonymous);
return counter;
}
}
El punto es que la clase local sustituye a todo el marco de pila y se inicializa en consecuencia cada vez que se invoca el método de Contador. Normalmente, el marco de pila incluye una referencia a 'esto', más argumentos de método, más variables locales. (El marco de pila también se extiende cuando se ingresa un bloque de control.)
En consecuencia, no tenemos un solo objeto que corresponda al contexto capturado, sino que tenemos un objeto por marco de pila capturado.
En base a esto, podemos usar el siguiente modelo mental: los marcos de pila se mantienen en el montón (en lugar de en la pila), mientras que la pila solo contiene punteros a los marcos de pila que están en el montón. Los métodos Lambda contienen un puntero al marco de la pila. Esto se hace utilizando la memoria administrada, por lo que el marco se queda en el montón hasta que ya no se necesita.
Obviamente, el compilador puede implementar esto utilizando solo el montón cuando se requiere el objeto Heap para admitir un cierre lambda.
Lo que me gusta de este modelo es que proporciona una imagen integrada para 'rendimiento de retorno'. Podemos pensar en un método de iterador (utilizando retorno de rendimiento) como si fuera un marco de pila creado en el montón y el puntero de referencia almacenado en una variable local en el llamador, para usarlo durante la iteración.
Una gran pregunta. No estoy seguro, pero sí, puedes mantener el marco de la pila en C#. Los generadores lo usan todo el tiempo (cosa LINQ para estructuras de datos) que dependen del rendimiento bajo el capó. Con suerte no estoy fuera de lugar. si lo estoy, aprenderé mucho. –
rendimiento convierte el método en una clase separada con una máquina de estado. La pila en sí misma no se mantiene, pero el estado de pila se mueve al estado de clase en una clase generada por el compilador – thecoop
@thecoop, ¿tiene un enlace que explique esto por favor? –