Почему ReSharper говорит мне "неявно зафиксированное закрытие"?

У меня есть следующий код:

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

Теперь я добавил комментарий к строке, в которой ReSharper предлагает изменения. Что это значит или зачем его нужно менять? implicitly captured closure: end, start

Ответ 1

Предупреждение сообщает вам, что переменные end и start остаются в живых, поскольку любой из lambdas внутри этого метода остается в живых.

Взгляните на короткий пример

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

Я получаю предупреждение "Неявно зафиксированное закрытие: g" на первой лямбда. Он говорит мне, что g не может быть собранным мусором, пока используется первая лямбда.

Компилятор генерирует класс для обоих лямбда-выражений и помещает все переменные в этот класс, которые используются в лямбда-выражениях.

Итак, в моем примере g и i хранятся в одном классе для выполнения моих делегатов. Если g - тяжелый объект с большим количеством ресурсов, оставленный позади, сборщик мусора не смог его вернуть, потому что ссылка в этом классе все еще жива, пока используется любое из лямбда-выражений. Таким образом, это потенциальная утечка памяти, и это является причиной предупреждения R #.

@splintor Как и в С#, анонимные методы всегда хранятся в одном классе для каждого метода, есть два способа избежать этого:

  • Используйте метод экземпляра вместо анонимного.

  • Разделить создание лямбда-выражений на два метода.

Ответ 2

Согласился с Питером Мортенсеном.

Компилятор С# генерирует только один тип, который инкапсулирует все переменные для всех лямбда-выражений в методе.

Например, с учетом исходного кода:

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

Компилятор генерирует тип, похожий на:

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

И метод Capture компилируется как:

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

Хотя вторая лямбда не использует x, сбор мусора не может быть собран, поскольку x скомпилирован как свойство сгенерированного класса, используемого в лямбда.

Ответ 3

Предупреждение действует и отображается в методах с более чем одной лямбда, и они фиксируют разные значения.

Когда вызывается метод, содержащий lambdas, создается объект, созданный компилятором, с помощью:

  • методы экземпляра, представляющие lambdas
  • представляющие все значения, захваченные любым из этих lambdas

В качестве примера:

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

Изучите сгенерированный код для этого класса (немного подобрав):

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

Обратите внимание, что экземпляр LambdaHelper создал как хранилища, так и p1 и p2.

Представьте, что:

  • callable1 сохраняет долговременную ссылку на свой аргумент, helper.Lambda1
  • callable2 не ссылается на свой аргумент, helper.Lambda2

В этой ситуации ссылка на helper.Lambda1 также косвенно ссылается на строку в p2, а это значит, что сборщик мусора не сможет его освободить. В худшем случае это утечка памяти/ресурсов. В качестве альтернативы он может удерживать объект дольше, чем в противном случае, что может повлиять на GC, если они получают повышение от gen0 до gen1.

Ответ 4

Для запросов Linq to Sql вы можете получить это предупреждение. Область лямбда может пережить этот метод из-за того, что запрос часто актуализируется после того, как метод выходит за рамки. В зависимости от вашей ситуации вы можете захотеть актуализировать результаты (т.е. Через .ToList()) внутри метода, чтобы разрешить GC на экземплярах экземпляра метода, захваченных в lambda L2S.

Ответ 5

Вы всегда можете выяснить причины предложений R #, просто нажав на подсказки, как показано ниже:

enter image description here

Эта подсказка направит вас сюда.


Эта проверка обращает ваше внимание на тот факт, что больше закрытия значения фиксируются, чем это явно видно, что имеет влияние на срок службы этих значений.

Рассмотрим следующий код:

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

В первом замыкании мы видим, что оба obj1 и obj2 явно фиксируются; мы можем увидеть это, просто взглянув на код. За второе закрытие, мы можем видеть, что obj1 явно захватывается, но ReSharper предупреждает нас, что obj2 неявно захвачен.

Это связано с деталями реализации в компиляторе С#. В течение компиляция, замыкания переписываются в классы с полями, которые содержат захваченные значения и методы, которые представляют само замыкание. Компилятор С# создаст только один такой закрытый класс для каждого метода, и если в методе определено более одного замыкания, то этот класс будет содержать несколько методов, по одному для каждого замыкания, а также будет включить все захваченные значения из всех замыканий.

Если мы посмотрим на код, который генерирует компилятор, он выглядит немного вот так (некоторые имена были очищены для облегчения чтения):

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

При запуске метода создается класс отображения, который фиксирует все значения для всех замыканий. Так что даже если значение не используется в одном из закрытий он все еще будет захвачен. Это "неявный" захват, который выделяет ReSharper.

Смысл этой проверки заключается в том, что неявно захваченный значение замыкания не будет собираться до тех пор, пока само замыкание это мусор Время жизни этого значения теперь связано с время жизни замыкания, которое явно не использует значение. Если замыкание долгое время, это может оказать негативное влияние на ваш код, особенно если захваченное значение очень большое.

Обратите внимание, что хотя это деталь реализации компилятора, он согласован между версиями и реализациями, такими как Microsoft (до и после Roslyn) или моно компилятор. Реализация должна работать как описано, чтобы правильно обрабатывать захват нескольких крышек тип значения. Например, если несколько замыканий захватывают int, то они должны захватывать один и тот же экземпляр, что может произойти только с один общий закрытый вложенный класс. Побочным эффектом этого является то, что время жизни всех захваченных значений теперь является максимальным временем жизни любого закрытие, которое захватывает любое из значений.