What is the lifetime of a delegate created by a lambda in C#?
Asked Answered
C

5

44

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?

Crucifix answered 8/6, 2011 at 14:44 Comment(0)
B
30

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?

Bullivant answered 8/6, 2011 at 15:29 Comment(2)
Very informative! But I'm a little confused by 'permitted'. You mean it's not right now but may be different in the future? I ran the code Action a1 = () => {}; Action a2 = () => {}; Console.WriteLine(object.ReferenceEquals(a1, a2)); and this returns false.Yearlong
@Jenix: By "permitted" I mean that the author of a C# compiler is permitted to compile your program such that it returns true or false at the compiler author's discretion. You must not rely upon the compiler having one or the other behavlor because this is documented as being allowed to change at any time.Bullivant
L
32

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.

Larkspur answered 8/6, 2011 at 14:48 Comment(5)
What exactly do they mean by "the same effects"? Calling GetCurrentMethod obviously doesn't have the same effects...Lohr
That sounds about right to me, though things also get a bit tricky if the lambda "captures" a type parameter of a generic method. Even if it makes use of no locals or parameters, it still might not be able to be cached in a static field.Bullivant
@Eric: One of my test cases uses public static Local<TValue> Instance<TScope>( Func<TScope> scope ), where () => this is passed to scope. In this case it is still returning the same delegate.Crucifix
I'm disliking the "(but not required)" so much right now, but oh well, I got my answer. :)Crucifix
@Steven: I assure you that it is not returning the same delegate if you create "()=>this" twice. You're doing something wrong if that's the result you're getting. How are you attempting to compare delegates for equality? You are aware that delegates have value, not reference equality, right?Bullivant
B
30

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?

Bullivant answered 8/6, 2011 at 15:29 Comment(2)
Very informative! But I'm a little confused by 'permitted'. You mean it's not right now but may be different in the future? I ran the code Action a1 = () => {}; Action a2 = () => {}; Console.WriteLine(object.ReferenceEquals(a1, a2)); and this returns false.Yearlong
@Jenix: By "permitted" I mean that the author of a C# compiler is permitted to compile your program such that it returns true or false at the compiler author's discretion. You must not rely upon the compiler having one or the other behavlor because this is documented as being allowed to change at any time.Bullivant
D
4

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

Deadwood answered 8/6, 2011 at 14:57 Comment(2)
That confirms Jon's reply: "A lambda expression which captures a local variable can't be cached".Crucifix
@Steven As well as, "A lambda expression which doesn't capture any variables is cached statically."Deadwood
S
3

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.

Subsocial answered 8/6, 2011 at 14:51 Comment(0)
S
1

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.

Sized answered 8/6, 2011 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.