Подробное объяснение смены переменных в закрытии

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

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

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

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

Ответ 1

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

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

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

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++;
        };
    }
}

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

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 и выполнить его через:)

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

  • В боксе не участвует
  • Нет указателей или любого другого небезопасного кода

EDIT: Здесь приведен пример двух делегатов, разделяющих переменную. Один делегат показывает текущее значение 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++;
        }
    }
}