在C#中由lambda创建的委托的生命周期是多少?

时间:2011-06-08 14:44:51

标签: c# delegates lambda

Lambda很不错,因为他们提供brevity and localityan extra form of encapsulation。而不必编写只使用一次lambda的函数。

虽然想知道它们是如何工作的,但我直觉地认为它们可能只创建了一次。这激发了我通过使用lambda作为其创建范围的标识符来创建一个允许to restrict the scope of a class member beyond private到一个特定范围的解决方案。

这种实施方式虽然可能有些过分(仍在研究),但证明我的假设是正确的。

一个较小的例子:

class SomeClass
{
    public void Bleh()
    {
        Action action = () => {};
    }

    public void CallBleh()
    {
        Bleh();  // `action` == {Method = {Void <SomeClass>b__0()}}
        Bleh();  // `action` still == {Method = {Void <SomeClass>b__0()}}
    }
}

lambda会返回一个新实例,还是保证总是相同?

5 个答案:

答案 0 :(得分:29)

这不是保证

从我记忆中的当前MS实现:

  • 不捕获任何变量的lambda表达式是静态缓存的
  • 只捕获“this”的lambda表达式可以基于每个实例捕获,但不是
  • 无法缓存捕获局部变量的lambda表达式
  • 两个具有完全相同的程序文本的lambda表达式不是别名;在某些情况下,他们可以,但是解决它们可能会非常复杂的情况
  • 编辑:正如Eric在评论中指出的那样,您还需要考虑为通用方法捕获类型参数。

编辑:C#4规范的相关文本见6.5.1节:

  

允许(但不是必需)将具有相同(可能为空)的捕获的外部变量实例集的语义相同的匿名函数转换为相同的委托类型以返回相同的委托实例。这里使用语义相同的术语来表示在所有情况下,匿名函数的执行在给定相同参数的情况下会产生相同的效果。

答案 1 :(得分:29)

根据你在这里提出的问题以及你对Jon的回答的评论,我认为你会混淆多方面的事情。为了确保清楚:

  • 支持给定lambda的委托的方法始终是相同的。
  • 方法支持代理为“相同”的lambda,它在词法上出现两次允许是相同的,但实际上不是在我们的实施中也是如此。
  • 为给定lambda创建的委托实例可能会也可能不会始终相同,具体取决于编译器对缓存它的智能程度。

所以如果你有类似的东西:

for(i = 0; i < 10; ++i)
    M( ()=>{} )

然后每次调用M时,都会获得委托的相同实例,因为编译器是智能的并且生成

static void MyAction() {}
static Action DelegateCache = null;

...
for(i = 0; i < 10; ++i)
{
    if (C.DelegateCache == null) C.DelegateCache = new Action ( C.MyAction )
    M(C.DelegateCache);
}

如果你有

for(i = 0; i < 10; ++i)
    M( ()=>{this.Bar();} )

然后编译器生成

void MyAction() { this.Bar(); }
...
for(i = 0; i < 10; ++i)
{
    M(new Action(this.MyAction));
}

每次都会使用相同的方法获得新的委托。

编译器允许(但实际上目前不允许)生成

void MyAction() { this.Bar(); }
Action DelegateCache = null;
...
for(i = 0; i < 10; ++i)
{
    if (this.DelegateCache == null) this.DelegateCache = new Action ( this.MyAction )
    M(this.DelegateCache);
}

在这种情况下,如果可能,您将始终获得相同的委托实例,并且每个委托都将使用相同的方法进行备份。

如果你有

Action a1 = ()=>{};
Action a2 = ()=>{};

然后在实践中,编译器将其生成为

static void MyAction1() {}
static void MyAction2() {}
static Action ActionCache1 = null;
static Action ActionCache2 = null;
...
if (ActionCache1 == null) ActionCache1 = new Action(MyAction1);
Action a1 = ActionCache1;
if (ActionCache2 == null) ActionCache2 = new Action(MyAction2);
Action a2 = ActionCache2;

但编译器允许检测到两个lambdas是否相同并生成

static void MyAction1() {}
static Action ActionCache1 = null;
...
if (ActionCache1 == null) ActionCache1 = new Action(MyAction1);
Action a1 = ActionCache1;
Action a2 = ActionCache1;

现在清楚了吗?

答案 2 :(得分:4)

不保证。

快速演示:

Action GetAction()
{
    return () => Console.WriteLine("foo");
}

请拨打两次,执行ReferenceEquals(a,b),然后您将获得true

Action GetAction()
{
    var foo = "foo";
    return () => Console.WriteLine(foo);
}

请拨打两次,执行ReferenceEquals(a,b),然后您将获得false

答案 3 :(得分:3)

我看到Skeet在我回答时跳了进来,所以我不会那么说。为了更好地理解你如何使用东西,我建议的一件事是熟悉逆向工程工具和IL。获取有问题的代码示例并对IL进行逆向工程。它将为您提供有关代码如何工作的大量信息。

答案 4 :(得分:1)

好问题。我没有“学术答案”,更多的是一个实际的答案:我可以看到编译器优化二进制文件以使用相同的实例,但我不会编写代码,假设它“保证”为同一个实例。

我至少向你倾诉,所以希望有人可以给你你正在寻找的学术答案。