Подробное объяснение захвата переменных в замыканиях - PullRequest
56 голосов
/ 26 марта 2011

Я видел бесчисленные сообщения о том, как захват переменных тянет переменные для создания замыкания, однако, похоже, что все они не дотягивают до конкретных деталей и называют все это «магией компилятора».

Я ищу четкое объяснение:

  1. Как локальные переменные на самом деле фиксируются.
  2. Разница (если есть) между типами значений захвата и ссылочными типами.
  3. И есть ли какие-либо блокировки в отношении типов значений.

Я бы предпочел ответ в терминах ценностей и указателей (ближе к сути происходящего внутри), хотя я приму четкий ответ, включающий также значения и ссылки.

1 Ответ

79 голосов
/ 26 марта 2011
  1. Это сложно. Приду на это через минуту.
  2. Нет никакой разницы - в обоих случаях захватывается сама переменная.
  3. Нет, бокс не происходит.

Вероятно, проще всего продемонстрировать, как захват работает на примере ...

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

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        Random rng = new Random();
        int counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", counter);
        return () =>
        {
            Console.WriteLine(counter);
            counter++;
        };
    }
}

Теперь вот что делает для вас компилятор - за исключением того, что он будет использовать «невыразимые» имена, которые в действительности не встречаются в C #.

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        ActionHelper helper = new ActionHelper();        
        Random rng = new Random();
        helper.counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", helper.counter);

        // Converts method group to a delegate, whose target will be a
        // reference to the instance of ActionHelper
        return helper.DoAction;
    }

    class ActionHelper
    {
        // Just for simplicity, make it public. I don't know if the
        // C# compiler really does.
        public int counter;

        public void DoAction()
        {
            Console.WriteLine(counter);
            counter++;
        }
    }
}

Если вы захватываете переменные, объявленные в цикле, вы получите новый экземпляр ActionHelper для каждой итерации цикла - так что вы эффективно захватите разные «экземпляры» переменных.

Ситуация усложняется, когда вы захватываете переменные из разных областей ... дайте мне знать, если вам действительно нужен такой уровень детализации, или вы можете просто написать некоторый код, декомпилировать его в Reflector и выполнить его:)

Обратите внимание, как:

  • Бокс не задействован
  • Нет указателей или других небезопасных кодов

РЕДАКТИРОВАТЬ: Вот пример двух делегатов, совместно использующих переменную. Один делегат показывает текущее значение counter, другой увеличивает его:

using System;

class Program
{
    static void Main(string[] args)
    {
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        int counter = 0;
        Action show = () => { Console.WriteLine(counter); };
        Action increment = () => { counter++; };
        return Tuple.Create(show, increment);
    }
}

... и расширение:

using System;

class Program
{
    static void Main(string[] args)
    {
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        ActionHelper helper = new ActionHelper();
        helper.counter = 0;
        Action show = helper.Show;
        Action increment = helper.Increment;
        return Tuple.Create(show, increment);
    }

    class ActionHelper
    {
        public int counter;

        public void Show()
        {
            Console.WriteLine(counter);
        }

        public void Increment()
        {
            counter++;
        }
    }
}
...