c# - ¿Por qué ReSharper me dice "cierre capturado implícitamente"?
linq (5)
Siempre puede encontrar una razón de R # sugerencias simplemente haciendo clic en las sugerencias que se muestran a continuación:
Esta pista te dirigirá here .
Esta inspección llama su atención sobre el hecho de que se están capturando más valores de cierre de lo que obviamente es visible, lo que tiene un impacto en la vida útil de estos valores.
Considere el siguiente código:
utilizando el sistema; clase pública clase1 {acción privada _someAction;
public void Method() { var obj1 = new object(); var obj2 = new object(); _someAction += () => { Console.WriteLine(obj1); Console.WriteLine(obj2); }; // "Implicitly captured closure: obj2" _someAction += () => { Console.WriteLine(obj1); }; } } In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For
En el segundo cierre, podemos ver que obj1 está siendo capturado explícitamente, pero ReSharper nos advierte que obj2 está siendo capturado implícitamente.
Esto se debe a un detalle de implementación en el compilador de C #. Durante la compilación, los cierres se reescriben en clases con campos que contienen los valores capturados y métodos que representan el cierre en sí. El compilador de C # solo creará una clase privada por método, y si se define más de un cierre en un método, esta clase contendrá múltiples métodos, uno para cada cierre, y también incluirá todos los valores capturados de todos los cierres.
Si miramos el código que genera el compilador, se parece un poco a esto (algunos nombres se han limpiado para facilitar la lectura):
clase pública clase1 {[CompilerGenerated] clase privada sellada <> c__DisplayClass1_0 {objeto público obj1; objeto público obj2;
internal void <Method>b__0() { Console.WriteLine(obj1); Console.WriteLine(obj2); } internal void <Method>b__1() { Console.WriteLine(obj1); } } private Action _someAction; public void Method() { // Create the display class - just one class for both closures var dc = new Class1.<>c__DisplayClass1_0(); // Capture the closure values as fields on the display class dc.obj1 = new object(); dc.obj2 = new object(); // Add the display class methods as closure values _someAction += new Action(dc.<Method>b__0); _someAction += new Action(dc.<Method>b__1); } } When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn''t used
en uno de los cierres, todavía será capturado. Esta es la captura "implícita" que ReSharper está resaltando.
La implicación de esta inspección es que el valor de cierre capturado implícitamente no se recolectará como basura hasta que el cierre en sí sea recolectado. La vida útil de este valor ahora está vinculada a la vida útil de un cierre que no usa explícitamente el valor. Si el cierre es de larga duración, esto podría tener un efecto negativo en su código, especialmente si el valor capturado es muy grande.
Tenga en cuenta que si bien este es un detalle de implementación del compilador, es consistente en todas las versiones e implementaciones como Microsoft (antes y después de Roslyn) o el compilador de Mono. La implementación debe funcionar como se describe para manejar correctamente los cierres múltiples que capturan un tipo de valor. Por ejemplo, si varios cierres capturan un int, entonces deben capturar la misma instancia, lo que solo puede ocurrir con una única clase anidada privada compartida. El efecto secundario de esto es que la vida útil de todos los valores capturados es ahora la vida útil máxima de cualquier cierre que capture cualquiera de los valores.
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, he agregado un comentario en la línea que ReSharper sugiere un cambio. ¿Qué significa o por qué debería cambiarse? implicitly captured closure: end, start
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 de aspecto:
[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 método de Capture
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 diferentes valores .
Cuando se invoca un método que contiene lambdas, se crea una instancia de un objeto generado por compilador con:
- Métodos de instancia que representan las lambdas.
- Campos que representan todos los valores capturados por cualquiera de esos 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 que la instancia de LambdaHelper
creado almacena p1
y p2
.
Imagina eso:
-
callable1
mantiene una referencia duradera a su argumento,helper.Lambda1
-
callable2
no guarda una 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 / recurso. Alternativamente, puede mantener el (los) objeto (s) vivo (s) más tiempo del necesario, lo que puede tener un impacto en GC si se promueven de gen0 a gen1.
La advertencia le dice que las variables end
y start
mantenerse vivas, ya que cualquiera de las lambdas dentro de este método permanece viva.
Echa un vistazo al pequeño 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();
}
Aparece una advertencia de "Cierre capturado implícitamente: g" en la primera lambda. Me está diciendo que g
no puede ser recogida de basura mientras la primera lambda esté en uso.
El compilador genera una clase para ambas expresiones lambda y coloca todas las variables en esa clase que se usan en las expresiones lambda.
Entonces, en mi ejemplo, g
y i
estamos en la misma clase para la ejecución de mis delegados. Si g
es un objeto pesado con una gran cantidad de recursos, el recolector de basura no podría recuperarlo, porque la referencia en esta clase sigue viva mientras alguna de las expresiones lambda esté en uso. Así que esta es una pérdida potencial de memoria, y esa es la razón de la advertencia de R #.
@splintor Al igual que 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.
Divide la creación de las expresiones lambda en dos métodos.
Para consultas de Linq a Sql, puede obtener 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, a través de .ToList ()) dentro del método para permitir GC en las vars de instancia del método capturadas en la lambda L2S.