Почему некоторые закрытия "дружелюбнее", чем другие?
Позвольте мне извиниться заранее - я, вероятно, собираю терминологию. У меня есть смутное понимание того, что такое закрытие, но не могу объяснить поведение, которое я вижу. По крайней мере, я думаю, что это проблема закрытия. Я искал в Интернете, но не нашел правильных ключевых слов, чтобы получить то, что я хочу.
В частности - у меня есть два блока кода, которые ДЕЙСТВИТЕЛЬНО ПОДОБНЫ (по крайней мере, для моих глаз). Во-первых:
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
Я вижу, что происходит, но я не понимаю, почему с ними обращаются по-разному. В моей голове - мне понравилось это оригинальное поведение, которое я видел, но более поздний пример меня смущает. Я надеюсь, что в этом есть смысл. Благодаря
Ответы
Ответ 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();
}