9

Based on the following question, I found some odd behaviour of the c# compiler.

The following is valid C#:

static void K() {}

static void Main()
{
  var k = new Action(new Action(new Action(K))));
}

What I do find strange is the compiler 'deconstructing' the passed delegate.

The ILSpy output is as follows:

new Action(new Action(new Action(null, ldftn(K)), ldftn(Invoke)).Invoke);

As one can see, it automatically decides to use the Invoke method of the delegate. But why?

As it is, the code is unclear. Do we have a triply-wrapped delegate (actual) or is the inner delegate just 'copied' to the outer ones (my initial thought).

Surely if the intent was like the compiler emitted the code, one should have written:

var k = new Action(new Action(new Action(K).Invoke).Invoke);

Similar to the decompiled code.

Can anyone justify the reason for this 'surprising' transformation?

Update:

I can only think of one possible use-case for this; delegate type conversion. Eg:

delegate void Baz();
delegate void Bar();
...
var k = new Baz(new Bar( new Action (K)));

Perhaps the compiler should emit a warning if the same delegate types are used.

Community
  • 1
  • 1
leppie
  • 115,091
  • 17
  • 196
  • 297
  • 2
    It's not clear what you mean by "is the inner delegate just 'copied' to the outer ones" - what exactly would you have expected? A single delegate instance? – Jon Skeet Nov 10 '11 at 10:10
  • 1
    Out of interest, where would you employ such a construct? It looks as though the "triply-wrapped delegate" will eat the un-astute programmer for breakfast... – MoonKnight Nov 10 '11 at 10:12
  • @JonSkeet: I expected it to be more akin to `new Action( a.Target, a.Method)` instead of `new Action(a.Invoke)`. IOW, no wrapping. – leppie Nov 10 '11 at 10:14
  • @Killercam: That **is** the ultimate question! Why should the C# compiler invite such code. – leppie Nov 10 '11 at 10:14
  • 1
    @Killercam: I would never write such code, in fact, I thought that it was a compiler error. Much to my surprise, it was not. As it stands now, there is only 1 use case I can think of, but that involves using different delegate types, hence acting as a delegate type converter. – leppie Nov 10 '11 at 10:17
  • Wondering if Eric is around ... – V4Vendetta Nov 10 '11 at 10:20

3 Answers3

5

The spec (section 7.6.10.5) says:

  • The new delegate instance is initialized with the same invocation list as the delegate instance given by E.

Now suppose the compiler translated it to something similar to your suggestion of:

new Action( a.Target, a.Method)

That would only ever create a delegate with an invocation list of a single method call. For a multi-cast delegate, it would violate the spec.

Sample code:

using System;

class Program
{
    static void Main(string[] args)
    {
        Action first = () => Console.WriteLine("First");
        Action second = () => Console.WriteLine("Second");

        Action both = first + second;
        Action wrapped1 =
            (Action) Delegate.CreateDelegate(typeof(Action),
                                             both.Target, both.Method);
        Action wrapped2 = new Action(both);

        Console.WriteLine("Calling wrapped1:");
        wrapped1();

        Console.WriteLine("Calling wrapped2:");
        wrapped2();
    }
}

Output:

Calling wrapped1:
Second
Calling wrapped2:
First
Second

As you can see, the real behaviour of the compiler matches the spec - your suggested behaviour doesn't.

This is partly due to the somewhat odd "sometimes single-cast, sometimes multi-cast" nature of Delegate, of course...

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Wait, that text is confusing. The new delegate will just be a wrapper, and will only have a single item in the invocation list. Else everything would be called twice. – leppie Nov 10 '11 at 10:33
  • I can see however that multicast delegates makes this a complex scenario. – leppie Nov 10 '11 at 10:35
  • `Console.WriteLine(wrapped2.GetInvocationList().Length);` prints 1. The spec is violated (or just incorrectly specified). – leppie Nov 10 '11 at 10:38
  • @leppie: It depends on whether you view "the invocation list" as simply "the methods which end up being invoked when the delegate is invoked" or the implementation details (with `GetInvocationList` etc). I think if you rely on *just what's in the spec*, the compiler behaviour is consistent. – Jon Skeet Nov 10 '11 at 10:38
  • It would have been clearer if it stated: 'The new delegate instance simply calls Invoke on the delegate given as E'. – leppie Nov 10 '11 at 10:40
  • What version of the C# spec are you using? Section 7 is just 'General Description' in version 4. – leppie Nov 10 '11 at 10:42
  • @leppie: Well that would have been more specific, of course. In a future version, perhaps it will create a shallow copy instead. And I'm using the C# 4 spec - section 7.6.10.5 is "Delegate Creation Expressions" – Jon Skeet Nov 10 '11 at 10:43
  • My spec is version 4 (EMCA 334), but definitely not C# 4. Where was this spec you have published/standardized? – leppie Nov 10 '11 at 10:47
  • Found it in the ECMA spec, section 14.5.10.3. – leppie Nov 10 '11 at 10:49
  • @leppie: ECMA edition 4 is C# version 2. I always use the MS spec these days :) – Jon Skeet Nov 10 '11 at 10:51
  • I assume that one has not gone through any standardization process, which is a pity. :( – leppie Nov 10 '11 at 10:53
3

When you try to treat a delegate as a method, the compiler actually uses the delegate's Invoke() method. So, for example, the two lines below compile to the exact same IL (both call Invoke()):

k();
k.Invoke();

I assume the oddity you're seeing is a consequence of this. The delegate constructor expects a method (or rather, a method group), but it gets a delegate instead. So it treats it as a method and uses the Invoke() method.

As for the meaning, it is delegate that calls delegate that calls the actual method. You can verify this yourself by accessing the delegate's Method and Target properties. In the case of the outer-most delegate, Method is Action.Invoke and Target the inner delegate.

svick
  • 236,525
  • 50
  • 385
  • 514
  • 1
    I kinda follow, but this design makes delegates more of a second class citizen. Like in some context `k` is really `k.Invoke`, but in other cases `k` is just a delegate. – leppie Nov 10 '11 at 10:28
  • I think that's because delegates are actually (almost) normal classes, as far as the CLR is concerned. To make them first-class citizen in C#, you have to resort to things like this. – svick Nov 10 '11 at 10:47
  • Normal classes are first class citizens, providing some magic functionality makes them second class (well that specific functionality). – leppie Nov 10 '11 at 10:51
2
  • delegate is a class
  • Action delegate has a constructor like so

    public extern Action(object @object, IntPtr method);

  • Since K is a static method there is no need to pass object to inner most action instance as first argument and hence it passes null

  • Since second argument is pointer to function therefore it passes pointer of K method using ldftn function
  • for the remaining Action instances the object is passed is inner Action and the second parameter is the Invoke method since when you call a delegate you're actually calling the Invoke method

Summary

var action = new Action(K) => Action action = new Action(null, ldftn(K))
new Action(action) => new Action(action, ldftn(Action.Invoke))

I hope this explains what is happening?

Muhammad Hasan Khan
  • 34,648
  • 16
  • 88
  • 131
  • The question I ask is exactly what you say in your last point. I know what is happening, but why? – leppie Nov 10 '11 at 10:12
  • If you read my question again, you will notice everything you say is already there. I understand **what** is happening. I want to know what the justification (or the why) for this. – leppie Nov 10 '11 at 10:19
  • 1
    @leppie how else should compiler transform your code? There is no such thing as new Action(blah) in MSIL. C# just provides you syntatic sugar. Its like asking why var i=3 is same as int i= 3 – Muhammad Hasan Khan Nov 10 '11 at 10:22