Чому 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


6
MyCodeSucks, будь ласка, виправте прийняту відповідь: помилка kevingessner є неправильною (як пояснено в коментарях), і якщо вона позначена як прийнята, введе в оману користувачів, якщо вони не помітять відповіді Console.
Альбірео

1
Ви також можете це побачити, якщо ви визначите свій список за межами спроби / лову і зробите все додавання у спробу / лову, а потім встановіть результати на інший об’єкт. Переміщення визначення / додавання в межах try / catch дозволить GC. Сподіваємось, це має сенс.
Micah Montoya

Відповіді:


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
Які можливі способи уникнути цього захоплення?
splintor

2
Дякую за чудову відповідь - я дізнався, що є причина використовувати неанонімний метод, навіть якщо він використовується лише в одному місці.
ScottRhee

1
@splintor Вмикає об'єкт всередині делегата або передає його як параметр. У вищенаведеному випадку, наскільки я можу сказати, бажана поведінка насправді полягає у посиланні на цей Randomпримірник.
Кейсі

2
@emodendroket Правильно, у цей момент ми говоримо про стиль коду та читаність. Про поле легше міркувати. Якщо тиск пам’яті або час життя об'єкта важливі, я вибрав би поле, інакше я б залишив його в більш короткому закритті.
yzorg

1
Мій випадок (сильно) спрощений зводився до фабричного методу, який створює Foo and 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 до Sql ви можете отримати це попередження. Обсяг лямбда може переживати метод через те, що запит часто актуалізується після того, як метод виходить за рамки. Залежно від вашої ситуації, ви, можливо, захочете актуалізувати результати (тобто через .ToList ()) в рамках методу, щоб дозволити GC на екземплярі методу vars, захоплені в лямбді 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, вони повинні захоплювати один і той же екземпляр, що може статися лише з одним спільним приватним вкладеним класом. Побічний ефект цього полягає в тому, що час життя всіх захоплених значень зараз є максимальним терміном експлуатації будь-якого закриття, яке фіксує будь-яке значення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.