What is the lifetime of a delegate created by a la

2019-01-16 23:36发布

问题:

Lambdas are nice, as they offer brevity and locality and an extra form of encapsulation. Instead of having to write functions which are only used once you can use a lambda.

While wondering how they worked, I intuitively figured they are probably only created once. This inspired me to create a solution which allows to restrict the scope of a class member beyond private to one particular scope by using the lambda as an identifier of the scope it was created in.

This implementation works, although perhaps overkill (still researching it), proving my assumption to be correct.

A smaller example:

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

Would the lambda ever return a new instance, or is it guaranteed to always be the same?

回答1:

Based on your question here and your comment to Jon's answer I think you are confusing multiple things. To make sure it is clear:

  • The method that backs the delegate for a given lambda is always the same.
  • The method that backs the delegate for "the same" lambda that appears lexically twice is permitted to be the same, but in practice is not the same in our implementation.
  • The delegate instance that is created for a given lambda might or might not always be the same, depending on how smart the compiler is about caching it.

So if you have something like:

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

then every time M is called, you get the same instance of the delegate because the compiler is smart and generates

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

If you have

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

then the compiler generates

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

You get a new delegate every time, with the same method.

The compiler is permitted to (but in fact does not at this time) generate

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

In that case you would always get the same delegate instance if possible, and every delegate would be backed by the same method.

If you have

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

Then in practice the compiler generates this as

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;

However the compiler is permitted to detect that the two lambdas are identical and generate

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

Is that now clear?



回答2:

It's not guaranteed either way.

From what I remember of the current MS implementation:

  • A lambda expression which doesn't capture any variables is cached statically
  • A lambda expression which only captures "this" could be captured on a per-instance basis, but isn't
  • A lambda expression which captures a local variable can't be cached
  • Two lambda expressions which have the exact same program text aren't aliased; in some cases they could be, but working out the situations in which they can be would be very complicated
  • EDIT: As Eric points out in the comments, you also need to consider type arguments being captured for generic methods.

EDIT: The relevant text of the C# 4 spec is in section 6.5.1:

Conversions of semantically identical anonymous functions with the same (possibly empty) set of captured outer variable instances to the same delegate types are permitted (but not required) to return the same delegate instance. The term semantically identical is used here to mean that execution of the anonymous functions will, in all cases, produce the same effects given the same arguments.



回答3:

No guarantees.

A quick demo:

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

Call this twice, do a ReferenceEquals(a,b), and you'll get true

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

Call this twice, do a ReferenceEquals(a,b), and you'll get false



回答4:

I see Skeet jumped in while I was answering, so I won't belabor that point. One thing I would suggest, to better understand how you are using things, is to get familiar with reverse engineering tools and IL. Take the code sample(s) in question and reverse engineer to IL. It will give you a great amount of information on how the code is working.



回答5:

Good question. I don't have an "academic answer," more of a practical answer: I could see a compiler optimizing the binary to use the same instance, but I wouldn't ever write code that assumes it's "guaranteed" to be the same instance.

I upvoted you at least, so hopefully someone can give you the academic answer you're looking for.