为什么有些闭包比其他闭包“更友好”?

34

提前道歉,我可能会误用术语。我对闭包的概念有一些模糊的了解,但无法解释我观察到的行为。至少我认为这是一个闭包问题。我已经在网上搜索过,但没有找到正确的关键词来获取我想要的内容。

具体地说,我有两个代码块非常相似(至少在我的眼中)。首先:

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's 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

这对我来说很有道理,也符合我所读的解释。在幕后,为每个委托/动作创建一个类,并为该类提供一个字段来保存“计数”的值。我睡觉时感觉很聪明!

但是 - 然后我尝试了这段非常相似的代码:

    // 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()似乎具有相同的count值!

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

我可以看到正在发生的事情-但我真的不明白为什么他们被区别对待。在我脑海中,我喜欢我看到的最初的行为,但后来的例子让我感到困惑。希望这有意义。谢谢。


27
第一个例子中有两个不同的int count变量声明(来自单独的方法调用)。你的第二个例子共享了相同的变量声明。如果int count是你主程序的字段,你的第一个例子将与第二个例子行为相同。 - Chris Sinclair
4个回答

50

你的第一个例子有两个不同的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;)对于该方法的调用是唯一的,然后当创建lambda委托时,每个委托都会将其自己唯一的count变量包含在闭包中:

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

27

闭包捕获一个变量。

当方法被调用时,会创建一个局部变量(还有其他创建局部变量的方式,但现在先忽略)。

在您的第一个示例中,您有两个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.

对抗

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的变量且不被多个对象共享,而在第二个例子中只有一个变量被多个对象共享,这对你来说有意义吗?


从 MSIL 的角度来看,还有其他东西可以创建局部变量,但现在我们先忽略它。 - Riki
2
@Felheart:我说话有些不太准确;我应该说有一些程序元素不是局部变量,但可以被捕获。按值传递的形式参数是可以被捕获的变量。此外,“this”值不是按值传递的变量,但可以被捕获。此外,在某种意义上,当泛型方法包含lambda时,类型参数也会被捕获,这不是任何类型的变量。 - Eric Lippert

6

区别在于一个示例中有一个委托,而另一个示例中有两个委托。

由于计数变量是局部的,每次调用时都会重新生成。由于只使用了一个委托(由于三元运算符),因此每个委托都得到了不同的变量副本。在另一个示例中,两个委托获取相同的变量。

三元运算符仅返回其两个参数之一,因此闭包按您的预期工作。在第二个示例中,您创建了两个共享相同“父级”计数变量的闭包,从而产生了不同的结果。

如果您这样看待它,可能会更清晰(这等效于您的第一个示例代码):

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

    // Each Action delegate has it's 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
嗯,我认为三元运算符和此无关;是两个方法调用创建了两个本地变量。如果删除三元运算符,你将得到相同的结果(除了每次都会说“Working 1”),但count的行为将保持不变。 - Chris Sinclair
@ChrisSinclair,没错。混淆似乎围绕着它看起来像有两个委托。不过谢谢提醒,我会更新以包含该信息! - BradleyDotNET

2

以下是另一个可能符合您需求的选择:

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(); 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接