¿Por qué ReSharper me dice "cierre capturado implícitamente"?
Tengo el siguiente código:
public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
Log("Calculating Daily Pull Force Max...");
var pullForceList = start == null
? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
: _pullForce.Where(
(t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 &&
DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();
_pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);
return _pullForceDailyMax;
}
Ahora, agregué un comentario en la línea que indica que ReSharper sugiere un cambio. ¿Qué significa o por qué sería necesario cambiarlo?implicitly captured closure: end, start
La advertencia le indica que las variables end
y start
permanecen activas ya que cualquiera de las lambdas dentro de este método permanece activa.
Eche un vistazo al breve ejemplo.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
int i = 0;
Random g = new Random();
this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}
Recibo una advertencia de "Cierre capturado implícitamente: g" en la primera lambda. Me dice que g
no se puede recolectar basura mientras la primera lambda esté en uso.
El compilador genera una clase para ambas expresiones lambda y coloca en esa clase todas las variables que se utilizan en las expresiones lambda.
Entonces, en mi ejemplo g
, se i
llevan a cabo en la misma clase para la ejecución de mis delegados. Si g
se trata de un objeto pesado al que le quedan muchos recursos, el recolector de basura no podrá recuperarlo, porque la referencia en esta clase sigue viva mientras cualquiera de las expresiones lambda esté en uso. Entonces, esta es una posible pérdida de memoria, y ese es el motivo de la advertencia de R#.
@splintor Como en C# los métodos anónimos siempre se almacenan en una clase por método, hay dos formas de evitar esto:
Utilice un método de instancia en lugar de uno anónimo.
Divida la creación de expresiones lambda en dos métodos.
De acuerdo con Peter Mortensen.
El compilador de C# genera solo un tipo que encapsula todas las variables para todas las expresiones lambda en un método.
Por ejemplo, dado el código fuente:
public class ValueStore
{
public Object GetValue()
{
return 1;
}
public void SetValue(Object obj)
{
}
}
public class ImplicitCaptureClosure
{
public void Captured()
{
var x = new object();
ValueStore store = new ValueStore();
Action action = () => store.SetValue(x);
Func<Object> f = () => store.GetValue(); //Implicitly capture closure: x
}
}
El compilador genera un tipo parecido a:
[CompilerGenerated]
private sealed class c__DisplayClass2
{
public object x;
public ValueStore store;
public c__DisplayClass2()
{
base.ctor();
}
//Represents the first lambda expression: () => store.SetValue(x)
public void Capturedb__0()
{
this.store.SetValue(this.x);
}
//Represents the second lambda expression: () => store.GetValue()
public object Capturedb__1()
{
return this.store.GetValue();
}
}
Y el Capture
método se compila como:
public void Captured()
{
ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
cDisplayClass2.x = new object();
cDisplayClass2.store = new ValueStore();
Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}
Aunque la segunda lambda no usa x
, no se puede recolectar basura ya que x
se compila como una propiedad de la clase generada utilizada en la lambda.
La advertencia es válida y se muestra en métodos que tienen más de una lambda y capturan valores diferentes .
Cuando se invoca un método que contiene lambdas, se crea una instancia de un objeto generado por el compilador con:
- métodos de instancia que representan las lambdas
- campos que representan todos los valores capturados por cualquiera de esas lambdas
Como ejemplo:
class DecompileMe
{
DecompileMe(Action<Action> callable1, Action<Action> callable2)
{
var p1 = 1;
var p2 = "hello";
callable1(() => p1++); // WARNING: Implicitly captured closure: p2
callable2(() => { p2.ToString(); p1++; });
}
}
Examine el código generado para esta clase (ordenado un poco):
class DecompileMe
{
DecompileMe(Action<Action> callable1, Action<Action> callable2)
{
var helper = new LambdaHelper();
helper.p1 = 1;
helper.p2 = "hello";
callable1(helper.Lambda1);
callable2(helper.Lambda2);
}
[CompilerGenerated]
private sealed class LambdaHelper
{
public int p1;
public string p2;
public void Lambda1() { ++p1; }
public void Lambda2() { p2.ToString(); ++p1; }
}
}
Tenga en cuenta la instancia de LambdaHelper
las tiendas creadas tanto p1
como p2
.
Imagina eso:
callable1
mantiene una referencia duradera a su argumento,helper.Lambda1
callable2
no guarda referencia a su argumento,helper.Lambda2
En esta situación, la referencia a helper.Lambda1
también hace referencia indirectamente a la cadena en p2
, y esto significa que el recolector de basura no podrá desasignarla. En el peor de los casos, es una pérdida de memoria/recursos. Alternativamente, puede mantener vivos los objetos por más tiempo del necesario, lo que puede tener un impacto en GC si se promueven de gen0 a gen1.
Para consultas de Linq a Sql, es posible que reciba esta advertencia. El alcance de la lambda puede sobrevivir al método debido al hecho de que la consulta a menudo se actualiza después de que el método está fuera del alcance. Dependiendo de su situación, es posible que desee actualizar los resultados (es decir, mediante .ToList()) dentro del método para permitir GC en las variables de instancia del método capturadas en la lambda L2S.