Замыкания в C#

    Перед прочтением статьи, ответьте на следующий вопрос — что будет напечатано, после исполнения следующего кода?

    P p = Console.WriteLine; // P объявлен как delegate void P();
    foreach (var i in new [] { 1, 2, 3, 4 }) {
      p += () => Console.Write(i);
    }
    p();

    (К сожалению, не хватает кармы для нормального оформления)

    Я провел опрос среди своих коллег, и только три человека из десяти смогли ответить правильно, причем только двое точно знали, что происходит и почему. Я, к своему стыду, правильного ответа не знал.
    Так вот, переведенный выше код покажет 4444. Однако, если этот код слегка изменить:

    P p = Console.WriteLine;
    foreach (var i in new int[] { 1, 2, 3, 4 }) {
      int j = i;
      p += () => Console.Write(j);
    }
    p();
    … то результат будет 1234. Давайте разберемся, почему так происходит.

    Наш анонимный метод (лямбда-выражение — это всего лишь «синтаксический сахар» для анонимных методов) использует в теле внешнюю переменную. Эта переменная становится захваченной (captured), и ее время жизни увеличивается до времени жизни делегата, которые ее использует. Это позволяет методу в принципе использовать значения захваченных переменных.

    Теперь в дело вступают инстанциация (instantiation) переменных и их область видимости (scope). В первом случае, переменная i инстанциируется один раз перед foreach. Фактически код

    foreach (var i in new[] { 1, 2, 3, 4 }) {
    //…
    }
    эквивалентен

    {
     int i;
     foreach (i in new[] { 1, 2, 3, 4 }) {
      //…
     }
    }
    Во втором случае переменная j создается и инстанциируется внутри цикла на каждой итерации. Переменные замыкаются в своей области видимости. Таким образом, в первом случае замкнутая переменная i будет изменятся при каждой итерации и к концу цикла будет равна четырем. Именно поэтому делегат выведет четыре четверки. Во втором случае, j будет замкнута внутри области видимости цикла и будет неизменна (фактически, будет созданно четыре экземпляра переменной j, каждая из которых получит свое значение), и делегат выведет 1234.

    Все это становится вполне очевидно, если мы заглянем внутрь сгенерированного компилятором кода, например, при помощи Reflector. Скомпилированный код первого примера (после некоторого причесываения) выглядит вот так:

    class DecodedFoo {
     private delegate void P();
     class Anonim {
      public int i;
      public void p()
      {
       Console.Write(i);
      }
     }
     public void Print() {
      P p = Console.WriteLine;
      var a = new Anonim();
      var array = new[] { 1, 2, 3, 4 };
      for (var i = 0; i < array.Length; i++) {
       a.i = array[i];
       p += a.p;
      }
      p();
     }
    }
    Весьма интересно, что компилятор развернул цикл foreach в for.

    Второй пример будет выглядеть вот так:

    class DecodedBar {
     private delegate void P();
     class Anonim {
      public int j;
      public void p() {
       Console.Write(j);
      }
     }
     public void Print() {
      P p = Console.WriteLine;
      foreach (var i in new [] { 1, 2, 3, 4 }) {
       var a = new Anonim();
       a.j = i;
       p += a.p;
      }
      p();
     }
    }
    Ссылки по теме для заинтересовавшихся:
    http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx

    http://blogs.msdn.com/oldnewthing/archive/2006/08/02/686456.aspx
    http://blogs.msdn.com/oldnewthing/archive/2006/08/03/687529.aspx
    http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx

    P.S. Resharper 4.0 умеет определять такие вот случаи, и для первого примера он выдает предупреждение «Access to modified closure» и предлагает переделать первый пример во второй. Но, однако, он не умеет отделять случаи, когда делегат вызывается внутри цикла, от слчуаев, когда делегат вызывается вне цикла.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 40
    • +3
      Мой первый пост. Не судите строго.
      • +2
        Карма выросла до 12 (спасибо!), но тэги <font> в предпросмотре все равно видны. Это нормально и можно опубликовать с нормальным форматированием?
      • +1
        > лямбда-выражение — это всего лишь «синтаксический сахар» для анонимных методов

        А по-моему всё как раз наоборот!
        • +2
          Синтаксический сахар (англ. syntactic sugar) — термин, обозначающий дополнения синтаксиса языка программирования, которые не добавляют новых возможностей, а делают использование языка более удобным для человека.
          Анонимные делегаты были и раньше, лямбда просто позволяет их проще использовать
          • +2
            Да, я не C#-разработчик, просто статью прочитал :) Для меня lambda — это как в Lisp, полноценный способ «создать процедуру». А в C# видимо терминология наоборот (по историческим причинам, видимо). Или в C# лямбда-выражения более «урезаны» (например, как в python, разрешено только одно выражение)?
            • НЛО прилетело и опубликовало эту надпись здесь
          • +2
            Когда читаю такие статьи: без вступления, без пояснения… невыносимо хочется спросить — Ты с кем сейчас разговаривал, папа? © :-)

            То есть с одной стороны вы написали что больше половины вашего окружения не понимают что есть по сути Замыкания (включая вас самих). А с другой — вы жахнули на Хабр свалку кода не удосужившись ничего внятно пояснить в целом. Зато разбавили его тучей умных слов. Ну… мы поняли как вы умны. :-)
            • +1
              Полагал, что блог.NET читают те, кто понимают, что такое делегаты, область видимости и другие умные слова в тексте. Исправлюсь, спасибо.
              • 0
                Что такое область видимости переменных скорее всего понимает любой квалифицированный программист. Это верно. А вот что означает термин 'Замыкания' — наверное не любой. Вы же вроде и сами признались в топике, что не имели достаточно понимания? Верно? Почему же тогда вас удивляет моё замечание? :)
                • +1
                  Мм… Теперь понял суть замечания. Наверное, я неправильно выбрал заголовок — слишком уж общий и предполагает действительно обзор со вступлением и т.п.
                  • +2
                    Если на пальцах, то замыкание — это (1) код и (2) переменные/значения, которые этот код использует. Это всё можно создавать динамически, обычно, при помощи привязывания функций к области видимости, примерно так:

                    function gen_function(x) { return function(y) { return x + y; }; }
                    add5 = gen_function(5);

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

                    Ну. Естественно, замыкание потом, после конструирования, можно выполнить. А вообще use wikipedia.

                    P.S. Замыкания — это очень распространённый инструмент. Объекты — это тоже своеобразные замыкания, например. Так что, любой программист должен про них знать. Особенно, если учесть, что замыкания — это удобный и мощный инструмент для параллельного программирования.
                    • 0
                      можно ссылку или обьяснить, как замыкания помогают в параллельном программировании?
                      • НЛО прилетело и опубликовало эту надпись здесь
                  • +1
                    Ну пост попал на главную страницу, поэтому лемминги вроде меня тоже полезли смотреть :)

                    Причем все слова в общем-то понятны, но… Я постарался проскроллить до комментов, в надежде, что пойму, о чем это все :)

                    И вообще, даешь холивар!!! :) PL/SQL рулит!!! :)
                • +1
                  Не знал, спасибо
                  • 0
                    Переодически вижу похожие конструкции в некоторых статьях. Конешно для общего развития и понимания это хорошо. НО… хоть блог и NET, но по опыту знаю, что многие слова и NET разработчики не знают.
                    PS К тому же в теории это всегда красиво, но на практике в коде не так часто встречаются конструкции такого рода.
                    • +1
                      Честно говоря, сам натолкнулся на подсказку Решарпера и заинетересовался. А код, где появилась подсказка, был весьма рабочим, что-то вроде:
                      foreach(int i in someList) {
                        someOtherList.ForEach(x=>x.Item=i); // <— здесь появилась подсказка
                      }
                      • 0
                        Замыкания — отличная динамическая замена шаблонам. Очень удобно бывает… Поэтому, надо обще развится, а потом начать применять их потихоньку.
                        • 0
                          А можно пример? Не совсем понял.
                          • +1
                            Как это в качестве шаблонов используется? Ну, я не знаю, вариантов же много. Например, можно фильтр создать из уже готовых функций.

                            function make_filter(condition) { return function(list) { return general_filter(list, condition); }; }
                            filter = make_filter(function(x) { return add5(x) < 20; })
                            filter(some_list_of_ints);
                            • 0
                              Какой шаблон имеется ввиду? C++ STL|ATL или шаблон проектирования?
                              • 0
                                Шаблон, который generic. Который генерирует в зависимости от различных параметров различные варианты различных объектов (в широком смысле слова).
                                • 0
                                  А код читается нормально при такой замене (из личного опыта)?
                                  • 0
                                    Нормально, если не злоупотреблять.
                              • 0
                                Каррирование :-)
                            • 0
                              Угу-угу… Когда в руках молоток, даже собственные ногти могут показаться гвоздями. :-)
                              • 0
                                Не во всех языках есть generic'и, так что, какое-то нерелевантное это замечание.
                              • 0
                                Каким шаблонам? Что сказать хотел?
                                • 0
                                  Что хотел, то и сказал.
                                  • 0
                                    Ещё раз переспрошу, если непонятно?
                                    Какой шаблон имеется ввиду? C++ STL|ATL или шаблон проектирования?
                            • 0
                              Спасибо за пост, я хотя бы понял, что замыканиями действительно увлекаться не стоит, ибо подобные примеры приводят к тому, что код становится абсолютно непонятным.
                              ЗЫ: Т.к. я читал лекции по замыканиям, я правильно догадался о 4444, я горд, занёс в блокнотик в качестве теста для соискателей работы :)
                              • –1
                                Интересно, что побуждает людей писать индийским кодом:
                                p += () => Console.Write(j);
                                • 0
                                  Видимо вам не нравится место «+= () =>». Соглашусь, тут немного коряво смотрится, но если вам вдруг нужен делегат типа:
                                  int delegate(int x, int y){ return x + y; }
                                  такая запись будет намного удобнее читаться:
                                  (x, y) => x + y
                                  меньше скобок, меньше лишних слов, а компилятор сам определит, что ему нужно.
                                  • –4
                                    Нет, я не про то. Конкретно подразумеваю выражение индусский код — код, написанный неестественным и неочевидным способом осложнённый для понимания. C++, к примеру, спроектирован с принципом избыточного программирования и в этом нет ничего плохого, но помимо этого он имеет кучу вот таких «фишек».

                                    C# создавался для платформы.NET, но всё же его пытались сделать более простым и понятным в использовании. Проблема в том, что от от версии к версии сам язык изменяется и на вопрос автора топика в общем смысле «что будет при исполнении такого кода» иногда придётся уточнять какая из дотнетовских платформ имелась в виду, ведь в них встроены разные компиляторы си шарповского языка.

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

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

                                    А вот из этого на месте автора я бы сделал вывод — «Теперь я знаю эту фишку, но поскольку подавляющее большинство её не знает, то я не буду её использовать, я не хочу писать индусский код (не путать с избыточным программированием)».
                                    • +1
                                      Ну, во-первых, код
                                      p += () => Console.Write(j);
                                      был показан только для примера. Мне нужен был делегат, который использовал замыкания, это первое, что пришло в голову, этакий proof of concept.
                                      Во-вторых, руководствуясь вашим принципом, мне не стоит тогда пользоваться анонимными делегатами, extension-методами, LINQ, даже, наверное, эвентами и собственными прерываниями, потому что многие из моего окружения не знают, к сожалению, как этим правильно пользоваться. Это, кстати, тема для отдельного поста, нужно ли и можно ли пользоваться advanced приемами и инструментами при программировании.
                              • 0
                                в книге трей кеша — данная ситуация хорошо рассматривается. (ускоренный курс для профессионалов)
                                • 0
                                  Подскажите, пожалуйста, где можно прочитать про var и про « p += () => Console.Write(j); „, конкретно про =>
                                  До этого момента думал, что делегаты я знаю )
                                  • +1
                                    Посмотрите тут, там есть ссылки на конкретные аспекты новововведений в C#3.0
                                    msdn.microsoft.com/en-us/library/bb383815.aspx
                                    • 0
                                      расписать можно примерно так:
                                      p += function() {Console.Write(j)}

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