Еще в далеком 2005 с выходом стандарта C# 2.0 появилась возможность передачи переменной в тело анонимного делегата посредством ее захвата (или замыкания, кому как угодно) из текущего контекста. В 2008 вышел в свет новый стандарт C# 3.0, принеся нам лямбды, пользовательские анонимные классы, LINQ запросы и многое другое. Сейчас на дворе январь 2017 и большинство C# разработчиков с нетерпением ждут релиз стандарта C# 7.0, который должен привнести много новых полезных «фич». А вот фиксить старые «фичи», никто особо не торопится. Поэтому способов случайно выстрелить себе в ногу по-прежнему хватает. Сегодня мы поговорим об одном из их, и связан он с не совсем очевидным механизмом захвата переменных в тело анонимных функций в языке C#.

Picture 1


Введение


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

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

И так, приступим:

void Foo()
{
  var actions = new List<Action>();
  for (int i = 0; i < 10; i++)
  {
    actions.Add(() => Console.WriteLine(i));
  }

  foreach(var a in actions)
  {
    a();
  }
}

А теперь внимание, ответ:

Ответ
В консоль будет выведено десять раз число десять:
10
10
10
10
10
10
10
10
10
10

Эта статья для тех, кто посчитал иначе. Давайте разберёмся в причинах такого поведения.

Почему так происходит?


При объявлении анонимной функции (это может быть анонимный делегат или лямбда) внутри вашего класса, на этапе компиляции будет объявлен еще один класс-контейнер, содержащий в себе поля для всех захваченных переменных и метод, содержащий тело анонимной функции. Для приведенного выше участка кода дизассемблированная структура программы после компиляции будет выглядеть так:

Picture 3



В данном случае метод Foo из приведенного в начале участка кода объявлен внутри класса Program. Для лямбды () => Console.WriteLine(i) компилятором был сгенерирован класс-контейнер c__DisplayClass1_0, а внутри него — поле i содержащее одноименную захваченную переменную и метод b__0 содержащий тело лямбды.

Давайте рассмотрим дизассемблированный IL код метода b__0 (тело лямбды) с моими комментариями:

Немного IL кода
.method assembly hidebysig instance void '<Foo>b__0'() cil managed
{
  .maxstack  8
  
  // Помещает на верх стека текущий экземпляр класса (аналог 'this').
  // Это необходимо для доступа к полям текущего класса.
  IL_0000:  ldarg.0
  
  // Помещает на верх стека значение поля 'i' 
  // экземпляра текущего класса.
  IL_0001:  ldfld int32 
    TestSolution.Program/'<>c__DisplayClass1_0'::i
  
  // Вызывает метод вывода строки в консоль.
  // В качестве аргументов передаются значения со стека.
  IL_0006:  call     void [mscorlib]System.Console::WriteLine(int32)
  
  // Выходит из метода.
  IL_000b:  ret
}

Все верно, это именно то, что мы делаем внутри лямбды, никакой магии. Идем дальше.

Как известно, тип int (полное название — Int32) является структурой, а значит при передаче куда-либо передается не ссылка на него в памяти, а копируется непосредственно его значение.

Копироваться значение переменной i должно (по логике вещей) во время создания экземпляра класса-контейнера. И если вы ответили неверно на мой вопрос в начале статьи, то вероятнее всего вы ожидали, что контейнер будет создан непосредственно перед объявлением лямбды в коде.

На самом деле переменная i после компиляции вообще не будет создана внутри метода Foo. Вместо этого будет создан экземпляр класса-контейнера c__DisplayClass1_0, а его поле i будет проинициализировано вместо локальной переменной i значением 0. Более того, везде, где до этого мы использовали локальную переменную i, теперь используется поле класса-контейнера.

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

В итоге мы получаем один экземпляр класса-контейнера на все итерации цикла for. А добавляя при каждой итерации в список actions новую лямбду, мы, по факту, добавляем в него одну и ту же ссылку на ранее созданный экземпляр класса-контейнера. В результате чего, когда мы обходим циклом foreach все элементы списка actions, то все они содержат один и тот же экземпляр класса-контейнера. А если учесть, что цикл for выполняет инкремент к значению итератора после каждой итерации (даже после последней), то значение поля i внутри класса контейнера после выхода из цикла становится равным десяти после выполнения цикла for.

Убедиться во всем мной вышесказанном можно, взглянув на дизассемблированный IL код метода Foo (естественно с моими комментариями):

Осторожно, много IL кода
.method private hidebysig instance void  Foo() cil managed
{
  .maxstack  3
  
  // -========== ОБЪЯВЛЕНИЕ ЛОКАЛЬНЫХ ПЕРЕМЕННЫХ ==========-
  .locals init(
    // Список 'actions'.
    [0] class [mscorlib]System.Collections.Generic.List'1
      <class [mscorlib]System.Action> actions,
    
    // Класс-контейнер для лямбды.
    [1] class TestSolution.Program/
      '<>c__DisplayClass1_0' 'CS$<>8__locals0',
    
    // Техническая переменная V_2 необходимая для временного
    // хранения результата операции суммирования.
    [2] int32 V_2,
    
    // Техническая переменная V_3 необходимая для хранения 
    // енумератора списка 'actions' во время обхода циклом 'foreach'.
    [3] valuetype
      [mscorlib]System.Collections.Generic.List'1/Enumerator<class
      [mscorlib]System.Action> V_3)

    
  // -================= ИНИЦИАЛИЗАЦИЯ =================-         
  // Создается экземпляр списка Actions и присваивается 
  // переменной 'actions'.
  IL_0000:  newobj     instance void class
    [mscorlib]System.Collections.Generic.List'1<class
    [mscorlib]System.Action>::.ctor()

  IL_0005:  stloc.0
  
  // Создается экземпляр класса-контейнера и 
  // присваивается в соответствующую локальную переменную.
  IL_0006:  newobj     instance void
    TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()
  IL_000b:  stloc.1
  
  // Загружается на стек ссылка экземпляра класса-контейнера.
  IL_000c:  ldloc.1
  
  // Число 0 загружается на стек.
  IL_000d:  ldc.i4.0
  
  // Присваивается со стека число 0 полю 'i' предыдущего
  // объекта на стеке (экземпляру класса-контейнера).
  IL_000e:  stfld      int32
    TestSolution.Program/'<>c__DisplayClass1_0'::i
  
  
  
  // -================= ЦИКЛ FOR =================-
  // Перепрыгивает к команде IL_0037.
  IL_0013:  br.s       IL_0037
  
  // Загружаются на стек ссылки списка 'actions' и
  // экземпляра класса-контейнера.
  IL_0015:  ldloc.0
  IL_0016:  ldloc.1
  
  // Загружается на стек ссылка на метод 'Foo' 
  // экземпляра класса-контейнера.
  IL_0017:  ldftn      instance void
    TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()
  
  // Создается экземпляр класса 'Action' и в него передается
  // ссылка на метод 'Foo' экземпляра класса-контейнера.
  IL_001d:  newobj     instance void
    [mscorlib]System.Action::.ctor(object, native int)
  
  // Вызывается метод 'Add' у списка 'actions' добавляя 
  // в него экземпляр класса 'Action'.
  IL_0022:  callvirt   instance void class
    [mscorlib]System.Collections.Generic.List'1<class
    [mscorlib]System.Action>::Add(!0)
  
  // Загружается на стек значение поля 'i' экземпляра 
  // класса-контейнера.
  IL_0027:  ldloc.1
  IL_0028:  ldfld      int32
    TestSolution.Program/'<>c__DisplayClass1_0'::i
  
  // Присваивается технической переменной 'V_2' значение поля 'i'.
  IL_002d:  stloc.2
  
  // Загружается на стек ссылка на экземпляр класса-контейнера
  // и значение технической переменной 'V_2'.
  IL_002e:  ldloc.1
  IL_002f:  ldloc.2
  
  // Загружается на стек число 1.
  IL_0030:  ldc.i4.1
  
  // Суммирует первые два значения на стеке и присваивает их третьему.
  IL_0031:  add
  
  // Присваивает со стека результат суммирования полю 'i'.
  // (по факту инкремент)
  IL_0032:  stfld      int32
    TestSolution.Program/'<>c__DisplayClass1_0'::i
  
  // Загружается значение поля 'i' экземпляра 
  // класса-контейнера на стек.
  IL_0037:  ldloc.1
  IL_0038:  ldfld      int32
    TestSolution.Program/'<>c__DisplayClass1_0'::i
  
  // Загружается на стек число 10.
  IL_003d:  ldc.i4.s   10
  
  // Если значение поля 'i' меньше числа 10, 
  // то перепрыгивает к команде IL_0015.
  IL_003f:  blt.s      IL_0015
  
  
  // -================= ЦИКЛ FOREACH =================-
  // Загружается на стек ссылка на список 'actions'.
  IL_0041:  ldloc.0
  
  // Технической переменной V_3 присваивается результат 
  // выполнения метода 'GetEnumerator' у списка 'actions'.
  IL_0042:  callvirt   instance valuetype
    [mscorlib]System.Collections.Generic.List'1/Enumerator<!0> class
    [mscorlib]System.Collections.Generic.List'1<class
    [mscorlib]System.Action>::GetEnumerator()

  IL_0047:  stloc.3
  
  // Инициализация блока try (цикл foreach преобразуется 
  // в конструкцию try-finally).
  .try
  {
    // Перепрыгивает к команде IL_0056.
    IL_0048:  br.s       IL_0056
    
    // Вызывает у переменной V_3 метод get_Current. 
    // Результат записывается на стек. 
    // (Ссылка на объект Action при текущей итерации).
    IL_004a:  ldloca.s   V_3
    IL_004c:  call       instance !0 valuetype
      [mscorlib]System.Collections.Generic.List'1/Enumerator<class
      [mscorlib]System.Action>::get_Current()
    
    // Вызывает у объекта Action текущей итерации метод Invoke.
    IL_0051:  callvirt   instance void
      [mscorlib]System.Action::Invoke()
    
    // Вызывает у переменной V_3 метод MoveNext. 
    // Результат записывается на стек.
    IL_0056:  ldloca.s   V_3
    IL_0058:  call       instance bool valuetype
      [mscorlib]System.Collections.Generic.List'1/Enumerator<class
      [mscorlib]System.Action>::MoveNext()
    
    // Если результат выполнения метода MoveNext не null, 
    // то перепрыгивает к команде IL_004a.
    IL_005d:  brtrue.s   IL_004a
    
    // Завершает выполнение блока try и перепрыгивает в finally.
    IL_005f:  leave.s    IL_006f
  }  // end .try
  finally
  {
    // Вызывает у переменной V_3 метод Dispose. 
    IL_0061:  ldloca.s   V_3
    IL_0063:  constrained. Valuetype
      [mscorlib]System.Collections.Generic.List'1/Enumerator<class
      [mscorlib]System.Action>

    IL_0069:  callvirt   instance void
      [mscorlib]System.IDisposable::Dispose()
    
    // Завершает выполнение блока finally.
    IL_006e:  endfinally
  }
  
  // Завершает выполнение текущего метода.
  IL_006f:  ret
}

Вывод


Товарищи из Microsoft утверждают, что это не баг, а фича, и это поведение было реализовано преднамеренно, с целью увеличения производительности работы программ. Больше информации по ссылке. На деле же это выливается в баги, и непонимание со стороны начинающих разработчиков.

Интересный факт заключается в том, что аналогичное поведение было и у цикла foreach до стандарта C# 5.0. Microsoft буквально засыпали жалобами о неинтуитивном поведении в баг-трекере, после чего с выходом стандарта C# 5.0 это поведение было изменено посредством объявления переменной итератора внутри каждой итерации цикла, а не перед ним на этапе компиляции, но для всех остальных конструкций циклов подобное поведение осталось без изменений. Подробнее об этом можно прочитать по ссылке в разделе Breaking Changes.

Вы спросите, как же избежать данной ошибки? На самом деле ответ очень простой. Необходимо следить за тем, где и какие переменные вы захватываете. Помните, класс-контейнер будет создан там, где вы объявили свою переменную, которую в дальнейшем будете захватывать. Если захват происходит в теле цикла, а переменная объявлена за его пределами, то необходимо переприсвоить ее внутри тела цикла в новую локальную переменную. Корректный вариант приведенного в начале примера мог бы выглядеть так:

void Foo()
{
  var actions = new List<Action>();
  for (int i = 0; i < 10; i++)
  {
    var index = i; // <=
    actions.Add(() => Console.WriteLine(index));
  }

  foreach(var a in actions)
  {
    a();
  }
}

Если выполнить данный код, то в консоль будут выведены числа от 0 до 9 как и ожидалось:

Вывод в консоль
0
1
2
3
4
5
6
7
8
9

Посмотрев на IL код цикла for из данного примера, мы увидим, что экземпляр класса-контейнера будет создаваться каждую итерацию цикла. Таким образом, список actions будет содержать ссылки на разные экземпляры с корректными значениями итераторов.

Еще немного IL кода
// -================= ЦИКЛ FOR =================-
// Перепрыгивает к команде IL_002d.
IL_0008:  br.s       IL_002d

// Создает экземпляр класса-контейнера и загружает ссылку на стек
IL_000a:  newobj     instance void
  TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()

IL_000f:  stloc.2
IL_0010:  ldloc.2

// Присваивает полю 'index' в классе-контейнере 
// значение переменной 'i'.
IL_0011:  ldloc.1
IL_0012:  stfld      int32
  TestSolution.Program/'<>c__DisplayClass1_0'::index

// Создает экземпляр класса 'Action' с ссылкой на метод 
// класса-контейнера и добавляет его в список 'actions'.
IL_0017:  ldloc.0
IL_0018:  ldloc.2
IL_0019:  ldftn      instance void
  TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()

IL_001f:  newobj     instance void
  [mscorlib]System.Action::.ctor(object, native int)

IL_0024:  callvirt   instance void class
  [mscorlib]System.Collections.Generic.List'1<class
  [mscorlib]System.Action>::Add(!0)
 
// Выполняет инкремент к переменной 'i'
IL_0029:  ldloc.1
IL_002a:  ldc.i4.1
IL_002b:  add
IL_002c:  stloc.1

// Загружает на стек значение переменной 'i'.
// В этот раз она уже не в классе-контейнере.
IL_002d:  ldloc.1

// Сравнивает значение переменной 'i' c числом 10.
// Если 'i < 10', то перепрыгивает к команде IL_000a.
IL_002e:  ldc.i4.s   10
IL_0030:  blt.s      IL_000a

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

Совсем недавно мы — разработчики статического анализатора PVS-Studio — реализовали очередную диагностику, направленную на поиск ошибок неправильного захвата переменных в анонимные функции внутри циклов. В свою же очередь спешу предложить вам проверить ваш код на наличие ошибок и опечаток нашим статическим анализатором.

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



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ivan Kishchenko. How to capture a variable in C# and not to shoot yourself in the foot

Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
Правильно ли вы ответили на вопрос в начале статьи (Что будет выведено в консоль?)

Проголосовал 351 человек. Воздержалось 39 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (41)


  1. Quilin
    27.01.2017 11:21
    +13

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


  1. aikixd
    27.01.2017 11:32
    +3

    На Джавескрипте такая же фигня есть. Как-то весьма давно наткнулся на эту фичу так глубоко, что думал уже весь модуль переписать. Если результаты работы замкнутой переменной не видно в одном месте, а тем более если обрабатываются они в разное время то пара веселых деньков вам обеспечено.
    Ответил бы неправильно, если бы не знал что вопрос с подвохом. Не ожидал этого в шарпе.


    P.S.
    Енумератор режет глаз. Впрочем как и энумератор. Используйте русский перечеслитель.


    1. mayorovp
      27.01.2017 11:39
      +2

      На джаваскрипте до появления let все было еще хуже. А на C# время жизни переменной всегда определялось блоком, а не методом. Вот и ноют иногда :)


    1. ggrnd0
      27.01.2017 11:39
      +1

      Лучше тогда итератор.


      1. aikixd
        27.01.2017 12:02

        Это разные вещи. Перечеслитель привязан к коллекциям, итератор к численному значению.


        1. ggrnd0
          27.01.2017 12:14
          +5

          Вероятно вы спутали IEnumerable и IEnumerator — 1й коллекция.


          Разница между Iterator и Enumerator в том что слово Iterator вошло в русский язык, а Enumerator нет.
          Значения же у них одинаковые.


          1. darkdaskin
            30.01.2017 16:11
            -1

            В C# итератором обычно называют реализацию IEnumerator с использованием yield return. Так что в контексте статьи не всякий перечислитель является итератором.


            1. mayorovp
              31.01.2017 06:42

              Реализация IEnumerator с использованием yield return всегда называлась "генератор", а не "итератор".


              1. ggrnd0
                31.01.2017 11:50

                К сожалению на русском MSDN итератор встречяается только в контексте IEnumerable+yield


                Но в js и java(там нет yield пока) это и правда называется генератор.
                Думаю в питоне и прочих тоже. Но MS — это MS=)


        1. sAntee
          27.01.2017 12:43
          +1

          С++ с вами не согласен :)


  1. mayorovp
    27.01.2017 11:46
    +7

    Это и правда фича. Потому что переменная цикла for — она принципиально общая для разных итераций и изменяемая!


    Рассмотрим такой код:


    for (int i=0; i<10; i++) {
      actions.Add(() => Console.WriteLine(i));
      if (i % 2 == 0) i++;
      actions.Add(() => Console.WriteLine(i));
    }

    Какое еще может быть тут поведение? Автоматически создавать новую переменную внутри цикла с тем же именем — нельзя, оператор i++ будет работать не так как ожидается.


    Захватывать переменную по значению? Тоже нельзя, они же всегда захватываются по ссылке, за что такое исключение переменным цикла?


    Даже создавать копию переменной только для захвата — тоже нельзя! Все из-за оператора i++ в теле цикла.


    А уж если вспомнить, что внутри замыкания переменную цикла тоже можно менять — становится ясным, что цикл for может работать только так как он работает сейчас.


    А вот с циклом foreach — ситуация другая. Там переменная цикла — неизменяемая, и никаких проблем в перетаскивании ее из внешнего блока во внутренний — нет.


    1. alexeykuzmin0
      27.01.2017 12:21
      +4

      А почему бы не дать возможность разработчику указать, как он хочет захватить — по ссылке или по значению? Так это сделано, например, в C++.


      1. areht
        28.01.2017 11:59

        Так в статье 2 примера, в одном захват по ссылке, во втором — нет. Для этого не нужны специальные операторы.


        1. sophist
          28.01.2017 13:26

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

          Например, так же, как это сделано для параметров функций — по умолчанию по значению, с ключевым словом ref — по ссылке.


          1. areht
            28.01.2017 14:25

            Ну… " var index = " недвусмысленно указывает, что я хочу значение.

            Вы хотите вместо этого явно где-то писать " ref int index "? Ну и выделите явно функцию, зачем вам замыкание?


        1. kishchenko
          30.01.2017 12:37
          +2

          В C# захват всегда происходит по ссылке, но во втором примере мы захватываем ссылку на копию объекта, сделанную внутри цикла, а в первом — на один объект для всех итераций цикла.


          1. areht
            30.01.2017 13:10
            -2

            > ссылку на копию объекта,

            А вы можете привести пример, где бы это имело значение (т.е. было бы важно)?


            1. kishchenko
              30.01.2017 13:29
              +3

              В статье есть 2 примера, которые наглядно демонстрируют отличия в поведении.
              Не вижу смысла пересказывать то, что уже написано.


              1. areht
                30.01.2017 20:09

                Тогда я не понял что именно вы хотели до меня донести


      1. Ryppka
        30.01.2017 12:34

        Потому что необходимость подумать и выбрать — это мука?


      1. Dreamer_other
        30.01.2017 12:34

        Очевидно потому, что в C# объекты передаются ТОЛЬКО по ссылке. Городить этот огород только для value типов смысла мало.


        1. mayorovp
          30.01.2017 13:07
          -1

          Нет, вы не поняли. Речь идет не о передаче объектов, а о захвате переменных. И в данном случае, захватываются по ссылке как объекты, так и value-типы! Посмотрите пример с циклом for: там индекс i вполне себе value-тип, но захвачен по ссылке.


  1. Deosis
    27.01.2017 11:50
    +1

    Почему компилятор использует пару инструкций stloc.3, ldloca.s, а не stloc.3, ldloc.3?


    1. mayorovp
      27.01.2017 11:52
      +2

      Потому что переменная номер 3 — это структура. А чтобы вызвать метод у структуры, нужен ее адрес.


  1. sand14
    27.01.2017 12:00
    +3

    Когда вышел C# 5.0, здесь уже была статья, что для цикла foreach поведением изменено таким образом, что на каждой итерации переменная для хранение текущего значения пересоздается (с возможностью через ключ компилятора вернуть старое поведение),
    и что для цикла for остается старое поведение — как раз по причине того, что счетчик for принципиально един для всех итераций (по сути — сахар для while с объявленной вне тела цикла переменной-счетчиком).


    Но за новую статью — спасибо. Более понятно и наглядно написано, как показалось.


  1. DrBAXA
    27.01.2017 12:10
    +2

    Тоже ответил неверно. Java просто не позволяет изменяемые переменные захватывать и думаю что правильно делает. Хотя и не понимаю почему нельзя просто копировать значение (ссылку если объект).


    1. DjoNIK
      27.01.2017 12:16
      +1

      Если копировать ссылку (можно переписать пример с reference type), будет такая же песня )) В контейнере на момент выполнения actions будет последний экземпляр. Вся суть в том, что используя замыкание, по факту, вы уже работаете не с оригинальной переменной, а с публичным полем класса-контейнера. Ну и как следствие — хранимое там значение регламентируется временем жизни экземпляра этого класса-контейнера.


    1. DieselMachine
      27.01.2017 12:54
      +4

      Просто копировать значение нельзя, потому что пишут например такой код, который в этом случае сломается

      var runInserts = true;
      Task.Run(() =>
      {
          while (runInserts)
              {
              }
      });
      ...
      runInserts = false;
      


    1. Sirikid
      27.01.2017 13:15

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


      int x = 0;
      Runnable y = () -> { ++x; }
      y.run();
      // x по прежнему 0, ведь y изменил скопированную переменную


  1. DjoNIK
    27.01.2017 12:12

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

    Выше уже неоднократно говорили о том, что различия в поведении замкнутых переменных для for и foreach вызваны различием в семантике обоих циклов.


  1. AxisPod
    27.01.2017 12:43
    +1

    Почему бага, статья при этом утверждает, что это фича. Да и собственно логичное поведение. Разве что управлять этим нельзя, ничего менять не надо. В крайнем случае сделать как в C++, только с текущим дефолтным поведением.


    1. JohnLivingston
      30.01.2017 12:39

      То, что вы считаете фичей, джависты считают ошибкой компиляции


      1. AxisPod
        30.01.2017 15:44

        Компилятор в C# честно предупреждает.


  1. vladimirkolyada
    27.01.2017 13:03

    Всего лет на 10 задержались:) https://blogs.msdn.microsoft.com/ruericlippert/2009/11/12/1094/


    1. Nagg
      27.01.2017 13:50
      +1

      На хабре тоже где-то столько же лет назад про это точно было уже :-).


      1. user004
        28.01.2017 17:08
        -1

        Дочитал до конца, надеялся до последнего на чудо. Увы.


  1. AlexS
    27.01.2017 18:02

    Имхо, точно написано — проблемы начинающих разработчиков. Каждый так или иначе стреляет себе в ногу, вот и вы попали :) Welcome to the club!


  1. creker
    28.01.2017 01:09

    Только недавно наткнулся на что-то подобное. Захватил в лямбде поле класса и удивлялся, чего у меня неправильно работает. Поле в процессе работы переписывалось, а в вместе с ним менялось и то, что попало в замыкание.

    Помог опыт Go, где есть точно такая же фича и цикл из примера будет работать точно так же.


    1. mayorovp
      28.01.2017 19:59
      -1

      Вообще-то, поле класса будет "замыкаться" одинаково вне зависимости от языка программирования! Разве что в Rust компилятор на что-нибудь ругнется...


  1. VAAKAraceGUM
    30.01.2017 12:34
    +1

    Планируете ли Вы в будущем выпустить плагины для таких IDE, как CLion и Rider для более удобной и продуктивной работы с PVS-Studio в Linux системах? Спасибо.


    1. kishchenko
      30.01.2017 12:56
      +1

      Здравствуйте!
      Интеграция с CLion поддерживается:
      http://www.viva64.com/ru/m/0036/#ID0ECCBI