Подтвердить что ты не робот

Почему некоторые закрытия "дружелюбнее", чем другие?

Позвольте мне извиниться заранее - я, вероятно, собираю терминологию. У меня есть смутное понимание того, что такое закрытие, но не могу объяснить поведение, которое я вижу. По крайней мере, я думаю, что это проблема закрытия. Я искал в Интернете, но не нашел правильных ключевых слов, чтобы получить то, что я хочу.

В частности - у меня есть два блока кода, которые ДЕЙСТВИТЕЛЬНО ПОДОБНЫ (по крайней мере, для моих глаз). Во-первых:

static void Main(string[] args)
{
    Action x1 = GetWorker(0);
    Action x2 = GetWorker(1);
}

static Action GetWorker(int k)
{
    int count = 0;

    // Each Action delegate has it own 'captured' count variable
    return k == 0 ? (Action)(() => Console.WriteLine("Working 1 - {0}",count++))
                  : (Action)(() => Console.WriteLine("Working 2 - {0}",count++));
}

Если вы запустите этот код и вызовите x1() и x2(), вы увидите, что они поддерживают отдельное значение "count".

    foreach(var i in Enumerable.Range(0,4))
    {
        x1(); x2(); 
    }

Выходы:

Working 1 - 0
Working 2 - 0
Working 1 - 1
Working 2 - 1
Working 1 - 2
Working 2 - 2
Working 1 - 3
Working 2 - 3

Это имеет смысл для меня и соответствует объяснениям, которые я прочитал. За кулисами создается класс для каждого делегата/действия, а классу присваивается поле для хранения значения "count". Я лег спать, чувствуя себя умным!

НО ТОГДА - я пробовал этот очень похожий код:

    // x3 and x4 *share* the same 'captured' count variable
    Action x3 = () => Console.WriteLine("Working 3 - {0}", count++);
    Action x4 = () => Console.WriteLine("Working 4 - {0}", count++);

И (как говорится в комментарии), здесь совершенно другое поведение. x3() и x4(), похоже, имеют ТОЛЬКО значение счетчика!

Working 3 - 0
Working 4 - 1
Working 3 - 2
Working 4 - 3
Working 3 - 4
Working 4 - 5
Working 3 - 6
Working 4 - 7

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

4b9b3361

Ответ 1

В вашем первом примере были две разные объявления переменных int count (из отдельных вызовов метода). В вашем втором примере используется одно и то же объявление переменной.

Ваш первый пример будет вести себя так же, как во втором примере int count было полем вашей основной программы:

static int count = 0;

static Action GetWorker(int k)
{
    return k == 0 ? (Action)(() => Console.WriteLine("Working 1 - {0}",count++))
                  : (Action)(() => Console.WriteLine("Working 2 - {0}",count++));
}

Выводится:

Working 1 - 0
Working 2 - 1
Working 1 - 2
Working 2 - 3
Working 1 - 4
Working 2 - 5
Working 1 - 6
Working 2 - 7

Вы можете упростить его и без тернарного оператора:

static Action GetWorker(int k)
{
    int count = 0;

    return (Action)(() => Console.WriteLine("Working {0} - {1}",k,count++));
}

Какие выходы:

Working 1 - 0
Working 2 - 0
Working 1 - 1
Working 2 - 1
Working 1 - 2
Working 2 - 2
Working 1 - 3
Working 2 - 3

Основная проблема заключается в том, что локальная переменная, объявленная в методе (в вашем случае int count = 0;), уникальна для этого вызова метода, тогда, когда создается делегат лямбда, каждый из них применяет закрытие вокруг своей уникальной count variable:

Action x1 = GetWorker(0); //gets a count
Action x2 = GetWorker(1); //gets a new, different count

Ответ 2

Закрытие захватывает переменную.

Локальная переменная создается, когда метод активируется путем вызова. (Есть другие вещи, которые создают локальные переменные, но на этот раз игнорировать.)

В первом примере у вас есть две активации GetWorker, и поэтому создаются две полностью независимые переменные с именем count. Каждый захвачен независимо.

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

Вот способ подумать об этом, который мог бы помочь:

class Counter { public int count; }
...
Counter Example1()
{
    return new Counter();
}
...
Counter c1 = Example1();
Counter c2 = Example1();
c1.count += 1;
c2.count += 2;
// c1.count and c2.count are different.

Vs

void Example2()
{
    Counter c = new Counter();
    Counter x3 = c; 
    Counter x4 = c;
    x3.count += 1;
    x4.count += 2;
    // x3.count and x4.count are the same.
}

Имеет ли смысл, почему в первом примере есть две переменные с именем count, которые не разделяются несколькими объектами, а во втором - только один, разделяемый несколькими объектами?

Ответ 3

Разница в том, что в одном примере у вас есть один делегат, другой у вас есть два.

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

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

Это может быть немного яснее, если вы посмотрите на него таким образом (это эквивалентный код для вашего первого образца):

static Action GetWorker(int k)
{
    int count = 0;
    Action returnDelegate

    // Each Action delegate has it own 'captured' count variable
    if (k == 0)
         returnDelegate = (Action)(() => Console.WriteLine("Working 1 - {0}",count++));
    else
         returnDelegate = (Action)(() => Console.WriteLine("Working 2 - {0}",count++));

    return returnDelegate
}

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

Ответ 4

Другая альтернатива (того, что, возможно, вы искали):

static Action<int> GetWorker()
{
    int count = 0;

    return k => k == 0 ? 
             Console.WriteLine("Working 1 - {0}",count++) : 
             Console.WriteLine("Working 2 - {0}",count++);
}

Тогда:

var x = GetWorker();

foreach(var i in Enumerable.Range(0,4))
{
    x(0); x(1);
}    

Или, может быть:

var y = GetWorker();
// and now we refer to the same closure
Action x1 = () => y(0);
Action x2 = () => y(1);

foreach(var i in Enumerable.Range(0,4))
{
    x1(); x2(); 
}

Или, может быть, с карри:

var f = GetWorker();
Func<int, Action> GetSameWorker = k => () => f(k);

//  k => () => GetWorker(k) will not work

Action z1 = GetSameWorker(0);
Action z2 = GetSameWorker(1);    

foreach(var i in Enumerable.Range(0,4))
{
    z1(); z2(); 
}