C#中由Lambda创建的委托的生命周期是什么?

44

在编程中,Lambda函数可以提供简洁性和局部性以及额外的封装形式。使用Lambda函数可以避免编写仅使用一次的函数。

当我思考它们的工作原理时,我直觉地认为它们可能只被创建一次。这启发了我创建一个解决方案,通过使用Lambda函数作为创建它的范围的标识符,可以将类成员的范围限制到一个特定的范围内,超出私有范围。

该实现可以正常工作,虽然可能过于复杂(仍在研究),但证明了我的假设是正确的。

以下是一个更小的示例:

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个回答

32

无论如何都不能保证。

根据我对当前 MS 实现的记忆:

  • 没有捕获任何变量的 lambda 表达式会被静态缓存
  • 只捕获 "this" 的 lambda 表达式可以基于每个实例进行捕获,但实际上并没有这样做
  • 捕获本地变量的 lambda 表达式无法缓存
  • 具有完全相同程序文本的两个 lambda 表达式不是别名;在某些情况下,它们可能是别名,但确定可能性非常复杂
  • 编辑:正如 Eric 在评论中指出的那样,您还需要考虑为泛型方法捕获的类型参数。

编辑:相关的 C# 4 规范文本在第6.5.1节中:

  

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


他们所说的“相同效果”究竟是什么意思?显然,调用GetCurrentMethod并不具有相同的效果... - user541686
这对我来说听起来大致正确,但如果lambda“捕获”了泛型方法的类型参数,事情也会变得有些棘手。即使它不使用任何本地变量或参数,它仍然可能无法被缓存在静态字段中。 - Eric Lippert
@Eric:我的一个测试用例使用了public static Local<TValue> Instance<TScope>( Func<TScope> scope ),其中将() => this传递给作用域。在这种情况下,它仍然返回相同的委托。 - Steven Jeuris
我现在非常讨厌那个“(但不是必需的)”,但没关系,我已经得到了答案。 :) - Steven Jeuris
3
@Steven: 我向你保证,如果你两次创建 "()=>this",它 不会 返回相同的委托。如果你得到了这样的结果,那么你肯定做错了什么。你是如何尝试比较委托是否相等的?你知道委托具有值相等性,而不是引用相等性,对吗? - Eric Lippert

30

根据您在这里提出的问题和对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;

然而编译器被允许检测到这两个lambda表达式是相同的并且生成

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

现在清楚了吗?


非常有用的信息!但是我有点困惑于“允许”的含义。您的意思是现在不行,但将来可能会有所不同吗?我运行了以下代码Action a1 = () => {}; Action a2 = () => {}; Console.WriteLine(object.ReferenceEquals(a1, a2));,结果返回false。 - Jenix
2
@Jenix:所谓“允许”,是指C#编译器的作者被允许编译您的程序,以便在编译器作者自行决定时返回truefalse。您不应该依赖于编译器具有其中一个行为,因为这被记录为随时可以更改。 - Eric Lippert

4

不提供保证。

一个快速演示:

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

调用两次,执行 ReferenceEquals(a,b),你会得到 true

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

调用两次后,进行ReferenceEquals(a,b)比较,结果会是false


这证实了Jon的回复:“捕获局部变量的lambda表达式无法被缓存”。 - Steven Jeuris
@Steven 此外,“不捕获任何变量的 Lambda 表达式会被静态缓存。” - Jay

3

我看到Skeet在我回答的时候已经提供了建议,所以我不再赘述。但是我建议您熟悉逆向工程工具和IL(Intermediate Language),以更好地理解您正在使用的内容。拿出相关的代码示例并反向工程到IL,这将为您提供大量关于代码如何工作的信息。


1

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

我至少给你点赞了,希望有人能给你提供你要找的学术答案。


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