Почему ReSharper говорит мне «неявно захваченное закрытие»?

296

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

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

PiousVenom
источник
6
MyCodeSucks, пожалуйста, исправьте принятый ответ: неправильный ответ kevingessner (как объяснено в комментариях) и пометка его как принятого введет пользователей в заблуждение, если они не заметят ответ консоли.
Albireo
1
Вы также можете увидеть это, если вы определите свой список вне try / catch и сделаете все свои добавления в try / catch, а затем установите результаты для другого объекта. Перемещение определения / добавления в пределах try / catch позволит GC. Надеюсь, это имеет смысл.
Мика Монтойя

Ответы:

391

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

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

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 Как и в C #, анонимные методы всегда хранятся в одном классе на метод, есть два способа избежать этого:

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

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

Приставка
источник
30
Каковы возможные способы избежать этого захвата?
сплинтер
2
Спасибо за этот отличный ответ - я узнал, что есть причина использовать неанонимный метод, даже если он используется только в одном месте.
Скоттри
1
@splintor Создает объект внутри делегата или передает его в качестве параметра. В вышеприведенном случае, насколько я могу судить, желаемое поведение на самом деле заключается в хранении ссылки на Randomэкземпляр.
Кейси
2
@emodendroket Правильно, сейчас мы говорим о стиле кода и удобочитаемости. Поле легче рассуждать. Если важно давление памяти или время жизни объекта, я бы выбрал поле, иначе я бы оставил его в более кратком закрытии.
yzorg
1
Мой случай (сильно) упрощенно сводился к фабричному методу, который создает Foo и Bar. Затем он подписывается на захват лямбок на события, обнаруживаемые этими двумя объектами, и, к удивлению, Foo сохраняет живые снимки с ламбы события Бар и наоборот. Я пришел из C ++, где этот подход работал бы просто отлично, и был более чем удивлен, обнаружив, что правила здесь другие. Я думаю, чем больше ты знаешь.
Dlf
35

Договорились с Питером Мортенсеном.

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

Например, учитывая исходный код:

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компилируется как свойство сгенерированного класса, используемого в лямбда.

Сообразительный ребенок
источник
31

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

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

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

Например:

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.

Дрю Ноакс
источник
если бы мы взяли ссылку на p1объект, callable2подобный следующему: callable2(() => { p2.ToString(); });- не вызовет ли это все еще ту же проблему (сборщик мусора не сможет ее освободить), которая LambdaHelperбудет по-прежнему содержаться p1и p2?
Антоний
1
Да, такая же проблема существует. Компилятор создает один объект захвата (т.е. LambdaHelperвыше) для всех лямбд в родительском методе. Таким образом, даже если callable2он не используется p1, он будет использовать один и тот же объект захвата callable1, и этот объект захвата будет ссылаться как на, так p1и на p2. Обратите внимание, что это действительно имеет значение только для ссылочных типов, и p1в этом примере это тип значения.
Дрю Ноакс
3

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

Джейсон Дюфаир
источник
2

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

введите описание изображения здесь

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


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

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

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 неявно захватывается.

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

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

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) или компилятор Mono. Реализация должна работать, как описано, чтобы правильно обрабатывать несколько замыканий, фиксирующих тип значения. Например, если несколько замыканий захватывают int, они должны захватывать один и тот же экземпляр, что может происходить только с одним общим закрытым вложенным классом. Побочным эффектом этого является то, что время жизни всех захваченных значений теперь является максимальным временем жизни любого замыкания, которое захватывает любое из значений.

Анатол
источник