¿Por qué ReSharper me dice "cierre capturado implícitamente"?

296

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é necesitaría ser cambiado?implicitly captured closure: end, start

Veneno piadoso
fuente
66
MyCodeSucks corrige la respuesta aceptada: la de kevingessner es incorrecta (como se explica en los comentarios) y marcarla como aceptada engañará a los usuarios si no notan la respuesta de Console.
Albireo
1
También puede ver esto si define su lista fuera de un try / catch y hace todo lo que agrega en el try / catch y luego establece los resultados en otro objeto. Mover la definición / adición dentro de try / catch permitirá GC. Esperemos que esto tenga sentido.
Micah Montoya

Respuestas:

391

La advertencia le dice que las variables endy startpermanecen vivas como cualquiera de las lambdas dentro de este método se mantienen vivas.

Echa 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 está diciendo que gno se puede recolectar 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 utilizan en las expresiones lambda.

Así que en mi ejemplo gy ise llevan a cabo en la misma clase para la ejecución de mis delegados. Si se gtrata de un objeto pesado con muchos recursos, el recolector de basura no podría reclamarlo, porque la referencia en esta clase aún está viva mientras esté en uso alguna de las expresiones lambda. Entonces, esta es una pérdida potencial de memoria, y esa es la razón 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:

  1. Use un método de instancia en lugar de uno anónimo.

  2. Divide la creación de las expresiones lambda en dos métodos.

Consola
fuente
30
¿Cuáles son las formas posibles de evitar esta captura?
splintor
2
Gracias por esta gran respuesta: he aprendido que hay una razón para usar un método no anónimo, incluso si se usa en un solo lugar.
ScottRhee
1
@splintor Instanciar el objeto dentro del delegado, o pasarlo como un parámetro en su lugar. Sin Randomembargo, en el caso anterior, por lo que puedo decir, el comportamiento deseado es en realidad mantener una referencia a la instancia.
Casey
2
@emodendroket Correcto, en este momento estamos hablando de estilo de código y legibilidad. Un campo es más fácil de razonar. Si la presión de memoria o la vida útil de los objetos son importantes, elegiría el campo, de lo contrario lo dejaría en el cierre más conciso.
yzorg
1
Mi caso (muy) simplificado se redujo a un método de fábrica que crea un Foo y un Bar. Luego suscribe la captura de lambas a eventos expuestos por esos dos objetos y, sorpresa sorpresa, el Foo mantiene vivas las capturas de la lamba del evento del Bar y viceversa. Vengo de C ++, donde este enfoque habría funcionado bien, y me sorprendió un poco descubrir que las reglas eran diferentes aquí. Cuanto más sepas, supongo.
dlf
35

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 similar 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 Capturemé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 se utiliza x, no se puede recolectar basura, ya que xse compila como una propiedad de la clase generada utilizada en la lambda.

Chico listo
fuente
31

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 (arreglado 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 LambdaHelpertiendas creadas tanto p1y p2.

Imagina eso:

  • callable1 mantiene una referencia de larga duración a su argumento, helper.Lambda1
  • callable2 no mantiene una referencia a su argumento, helper.Lambda2

En esta situación, la referencia a helper.Lambda1también hace referencia indirecta a la cadena 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 los objetos vivos más tiempo de lo que se necesita, lo que puede tener un impacto en GC si son promovidos de gen0 a gen1.

Drew Noakes
fuente
si sacamos la referencia de p1de callable2esta manera: callable2(() => { p2.ToString(); });- ¿esto todavía no causaría el mismo problema (el recolector de basura no podrá desasignarlo) LambdaHelperque todavía contendrá p1y p2?
Antony
1
Sí, existiría el mismo problema. El compilador crea un objeto de captura (es decir, LambdaHelperarriba) para todas las lambdas dentro del método padre. Entonces, incluso si callable2no se usara p1, compartiría el mismo objeto de captura que callable1, y ese objeto de captura haría referencia a ambos p1y p2. Tenga en cuenta que esto realmente solo importa para los tipos de referencia, y p1en este ejemplo es un tipo de valor.
Drew Noakes
3

Para consultas de Linq a Sql, puede recibir 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 los vars de instancia del método capturados en la lambda L2S.

Jason Dufair
fuente
2

Siempre puede descubrir las razones de las sugerencias de R # simplemente haciendo clic en las sugerencias como se muestra a continuación:

ingrese la descripción de la imagen aquí

Esta pista te dirigirá aquí .


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:

using System; 
public class Class1 {
    private Action _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);
        };
    }
}

En el primer cierre, vemos que tanto obj1 como obj2 están siendo capturados explícitamente; podemos ver esto solo mirando el código. Para el segundo cierre, podemos ver que obj1 se está capturando explícitamente, pero ReSharper nos advierte que obj2 se está capturando 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 de este tipo 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):

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object 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);
    }
}

Cuando se ejecuta el método, crea la clase de visualización, que captura todos los valores, para todos los cierres. Entonces, incluso si un valor no se usa en uno de los cierres, aún se capturará. Esta es la captura "implícita" que ReSharper está destacando.

La implicación de esta inspección es que el valor de cierre capturado implícitamente no se recolectará basura hasta que el cierre en sí mismo se recolecte. La vida útil de este valor ahora está vinculada a la vida útil de un cierre que no utiliza 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 coherente 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, deben capturar la misma instancia, lo que solo puede suceder con una sola clase privada compartida anidada. 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.

anatol
fuente