Por que o ReSharper me diz "fechamento implicitamente capturado"?

296

Eu tenho o seguinte 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;
}

Agora, adicionamos um comentário na linha que o ReSharper está sugerindo uma mudança. O que significa ou por que precisaria ser alterado?implicitly captured closure: end, start

PiousVenom
fonte
6
MyCodeSucks, por favor, corrija a resposta aceita: a do kevingessner está errada (como explicado nos comentários) e tê-la marcada como aceita irá enganar os usuários se eles não perceberem a resposta do console.
Albireo
1
Você também pode ver isso se definir sua lista fora de uma tentativa / captura e adicionar todos os itens na tentativa / captura e, em seguida, definir os resultados para outro objeto. Mover a definição / adição dentro do try / catch permitirá ao GC. Espero que isso faça sentido.
Micah Montoya

Respostas:

391

O aviso informa que as variáveis ende startpermanecer vivo como qualquer um dos lambdas dentro esta estadia método vivo.

Dê uma olhada no pequeno exemplo

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();
}

Recebo um aviso "Fechamento implicitamente capturado: g" no primeiro lambda. Está me dizendo que gnão pode ser coletado o lixo enquanto o primeiro lambda estiver em uso.

O compilador gera uma classe para ambas as expressões lambda e coloca todas as variáveis ​​nessa classe que são usadas nas expressões lambda.

Então, no meu exemplo ge isão realizadas na mesma classe para execução dos meus delegados. Se gfor um objeto pesado com muitos recursos deixados para trás, o coletor de lixo não pôde recuperá-lo, porque a referência nesta classe ainda está ativa enquanto alguma das expressões lambda estiver em uso. Portanto, esse é um vazamento de memória em potencial e esse é o motivo do aviso de R #.

@ splintor Como em C #, os métodos anônimos são sempre armazenados em uma classe por método, existem duas maneiras de evitar isso:

  1. Use um método de instância em vez de um método anônimo.

  2. Divida a criação das expressões lambda em dois métodos.

Console
fonte
30
Quais são as formas possíveis de evitar essa captura?
Splintor
2
Obrigado por esta ótima resposta - eu aprendi que há uma razão para usar o método não anônimo, mesmo que seja usado em apenas um lugar.
21926 ScottRhee #
1
@splintor Instancie o objeto dentro do delegado ou passe-o como um parâmetro. No caso acima, até onde eu sei, o comportamento desejado é realmente manter uma referência à Randominstância.
Casey #
2
@emodendroket Correto, neste momento estamos falando de estilo e legibilidade do código. Um campo é mais fácil de raciocinar. Se a pressão da memória ou a vida útil do objeto são importantes, eu escolhi o campo, caso contrário, deixaria no fechamento mais conciso.
yzorg 30/12/14
1
Meu caso (fortemente) simplificado se resumia a um método de fábrica que cria um Foo e um Bar. Em seguida, assina a captura de lambas para eventos expostos por esses dois objetos e, surpresa surpresa, o Foo mantém as capturas da lamba do evento do Bar vivas e vice-versa. Eu venho de C ++, onde essa abordagem teria funcionado muito bem e fiquei mais do que um pouco surpresa ao descobrir que as regras eram diferentes aqui. Quanto mais você sabe, eu acho.
DLF
35

Concordou com Peter Mortensen.

O compilador C # gera apenas um tipo que encapsula todas as variáveis ​​para todas as expressões lambda em um método.

Por exemplo, dado o código fonte:

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
    }
}

O compilador gera um tipo parecido com:

[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();
  }
}

E o Capturemétodo é compilado 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));
}

Embora o segundo lambda não use x, ele não pode ser coletado como lixo, pois xé compilado como uma propriedade da classe gerada usada no lambda.

Smartkid
fonte
31

O aviso é válido e exibido em métodos que possuem mais de um lambda e eles capturam valores diferentes .

Quando um método que contém lambdas é chamado, um objeto gerado pelo compilador é instanciado com:

  • métodos de instância que representam as lambdas
  • campos que representam todos os valores capturados por qualquer uma dessas lambdas

Como um exemplo:

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 o código gerado para esta classe (arrumado um pouco):

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; }
    }
}

Observe a instância de LambdaHelperlojas criadas, tanto p1e p2.

Imagine isso:

  • callable1 mantém uma referência duradoura ao seu argumento, helper.Lambda1
  • callable2 não mantém uma referência ao seu argumento, helper.Lambda2

Nessa situação, a referência helper.Lambda1também referencia indiretamente a cadeia de caracteres p2e isso significa que o coletor de lixo não poderá desalocá-la. Na pior das hipóteses, é um vazamento de memória / recursos. Como alternativa, ele pode manter o (s) objeto (s) ativo (s) por mais tempo do que o necessário, o que pode afetar o GC se eles forem promovidos do gen0 para o gen1.

Drew Noakes
fonte
se tirou a referência p1de callable2como isto: callable2(() => { p2.ToString(); });- que ainda não esta causar o mesmo problema (coletor de lixo não será capaz de desalocar-lo) como LambdaHelperainda irá conter p1e p2?
Antony
1
Sim, o mesmo problema existiria. O compilador cria um objeto de captura (ou seja, LambdaHelperacima) para todas as lambdas no método pai. Portanto, mesmo que callable2não fosse utilizado p1, ele compartilharia o mesmo objeto de captura que callable1, e esse objeto de captura faria referência a ambos p1e p2. Observe que isso realmente importa apenas para tipos de referência e p1, neste exemplo, é um tipo de valor.
Drew Noakes #
3

Para consultas de Linq para Sql, você pode receber este aviso. O escopo do lambda pode sobreviver ao método devido ao fato de que a consulta geralmente é atualizada depois que o método está fora do escopo. Dependendo da sua situação, convém atualizar os resultados (por exemplo, via .ToList ()) dentro do método para permitir o GC nos vars de instância do método capturados no lambda L2S.

Jason Dufair
fonte
2

Você sempre pode descobrir algumas razões para sugerir o R ​​# clicando nas dicas, como mostrado abaixo:

insira a descrição da imagem aqui

Esta dica irá direcioná-lo aqui .


Essa inspeção chama a atenção para o fato de que mais valores de fechamento estão sendo capturados do que é obviamente visível, o que afeta a vida útil desses valores.

Considere o seguinte 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);
        };
    }
}

No primeiro fechamento, vemos que obj1 e obj2 estão sendo explicitamente capturados; podemos ver isso apenas olhando o código. Para o segundo fechamento, podemos ver que o obj1 está sendo capturado explicitamente, mas o ReSharper está nos alertando que o obj2 está sendo capturado implicitamente.

Isso ocorre devido a um detalhe de implementação no compilador C #. Durante a compilação, os fechamentos são reescritos em classes com campos que mantêm os valores capturados e métodos que representam o próprio fechamento. O compilador C # criará apenas uma classe privada por método e, se mais de um fechamento for definido em um método, essa classe conterá vários métodos, um para cada fechamento, e também incluirá todos os valores capturados de todos os fechamentos.

Se observarmos o código que o compilador gera, ele se parece um pouco com isso (alguns nomes foram limpos para facilitar a leitura):

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);
    }
}

Quando o método é executado, ele cria a classe de exibição, que captura todos os valores, para todos os fechamentos. Portanto, mesmo que um valor não seja usado em um dos fechamentos, ele ainda será capturado. Esta é a captura "implícita" que o ReSharper está destacando.

A implicação dessa inspeção é que o valor do fechamento capturado implicitamente não será coletado como lixo até que o próprio fechamento seja coletado. O tempo de vida desse valor agora está vinculado ao tempo de vida de um fechamento que não usa explicitamente o valor. Se o fechamento for prolongado, isso poderá ter um efeito negativo no seu código, especialmente se o valor capturado for muito grande.

Observe que, embora esse seja um detalhe de implementação do compilador, ele é consistente entre versões e implementações, como o Microsoft (antes e depois do Roslyn) ou o compilador Mono. A implementação deve funcionar como descrito para lidar corretamente com vários fechamentos que capturam um tipo de valor. Por exemplo, se vários fechamentos capturarem um int, eles deverão capturar a mesma instância, o que só pode acontecer com uma única classe aninhada privada compartilhada. O efeito colateral disso é que a vida útil de todos os valores capturados agora é a vida útil máxima de qualquer fechamento que captura qualquer um dos valores.

anatol
fonte