﻿# Как замкнуть переменную в C\# и не выстрелить себе в ногу

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

![0468_LambdaCaptureError_ru/image1.png](https://import.viva64.com/docx/blog/0468_LambdaCaptureError_ru/image1.png)

## Введение

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

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

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

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

А теперь внимание, ответ\. В консоль будет выведено десять раз число десять:

```cpp
10
10
10
10
10
10
10
10
10
10
```

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

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

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

![0468_LambdaCaptureError_ru/image2.png](https://import.viva64.com/docx/blog/0468_LambdaCaptureError_ru/image2.png)



В данном случае метод _Foo_ из приведенного в начале участка кода объявлен внутри класса _Program_\. Для лямбды _\(\) \=\> Console\.WriteLine\(i\)_ компилятором был сгенерирован класс\-контейнер _c\_\_DisplayClass1\_0_, а внутри него \- поле _i_ содержащее одноименную захваченную переменную и метод _b\_\_0_ содержащий тело лямбды\. 

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

```cpp
.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_ \(естественно с моими комментариями\):

```cpp
.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 утверждают, что это не баг, а фича, и это поведение было реализовано преднамеренно, с целью увеличения производительности работы программ\. Больше информации по [ссылке](https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-anonymous-methods-are-not-closures)\. На деле же это выливается в баги, и непонимание со стороны начинающих разработчиков\.

Интересный факт заключается в том, что аналогичное поведение было и у цикла _foreach_ до стандарта C\# 5\.0\. Microsoft буквально засыпали жалобами о неинтуитивном поведении в баг\-трекере, после чего с выходом стандарта C\# 5\.0 это поведение было изменено посредством объявления переменной итератора внутри каждой итерации цикла, а не перед ним на этапе компиляции, но для всех остальных конструкций циклов подобное поведение осталось без изменений\. Подробнее об этом можно прочитать по [ссылке](https://visualstudiomagazine.com/articles/2012/11/01/more-than-just-async.aspx) в разделе _Breaking Changes_\.

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

```cpp
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 как и ожидалось:

```cpp
0
1
2
3
4
5
6
7
8
9
```

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

```cpp
// -================= ЦИКЛ 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](https://pvs-studio.ru/ru/pvs-studio/) \- реализовали очередную [диагностику](https://pvs-studio.ru/ru/docs/warnings/v3129/), направленную на поиск ошибок неправильного захвата переменных в анонимные функции внутри циклов\. В свою же очередь спешу предложить вам проверить ваш код на наличие ошибок и опечаток нашим статическим анализатором\.

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