45

Consider the code below. Looks like perfectly valid C# code right?

//Project B
using System;
public delegate void ActionSurrogate(Action addEvent);
//public delegate void ActionSurrogate2();
// Using ActionSurrogate2 instead of System.Action results in the same error
// Using a dummy parameter (Action<double, int>) results in the same error

// Project A
public static class Class1 {
    public static void ThisWontCompile() {
        ActionSurrogate b = (a) =>
                            {
                                a(); // Error given here
                            };
    }
}

I get a compiler error 'Delegate 'Action' does not take 0 arguments.' at the indicated position using the (Microsoft) C# 4.0 compiler. Note that you have to declare ActionSurrogate in a different project for this error to manifest.

It gets more interesting:

// Project A, File 1
public static class Class1 {
    public static void ThisWontCompile() {
        ActionSurrogate b = (a) => { a(); /* Error given here */ };
        ActionSurrogate c = (a) => { a(); /* Error given here too */ };
        Action d = () => { };
        ActionSurrogate c = (a) => { a(); /* No error is given here */ };
    }
}

Did I stumble upon a C# compiler bug here?

Note that this is a pretty annoying bug for someone who likes using lambdas a lot and is trying to create a data structures library for future use... (me)

EDIT: removed erronous case.

I copied and stripped my original project down to the minimum to make this happen. This is literally all the code in my new project.

Alex ten Brink
  • 899
  • 2
  • 9
  • 19
  • It's *very* interesting that in your second example, Class2 compiles, while in your first example *the exact same code* breaks. – Anon. Dec 17 '10 at 01:10
  • Is it possible you have a different definition of `Action` somewhere? – Gabe Dec 17 '10 at 01:15
  • @Anon.: Class2 was originally in Project B, which is why no errors were reported. I just tested it and using a different file makes no difference within Project A. – Alex ten Brink Dec 17 '10 at 01:39
  • I got a more complex version of this: http://stackoverflow.com/questions/3276578/compilation-fails-if-delegate-definitions-is-put-in-another-project ... and filed as an issue on MS connect: https://connect.microsoft.com/VisualStudio/feedback/details/577938/delegate-references-bug-in-the-c-compiler .... However, this is a much much simpler steps to reproduce. Hopefully it'll get fixed in SP1. – chakrit Jan 19 '11 at 20:50
  • Looking at the code Jon Skeet posted, it seems you experienced the exact same bug, which makes me curious if they ever found out what caused the bug after reading your report... – Alex ten Brink Jan 19 '11 at 21:33

3 Answers3

68

FINAL UPDATE:

The bug has been fixed in C# 5. Apologies again for the inconvenience, and thanks for the report.


Original analysis:

I can reproduce the problem with the command-line compiler. It certainly looks like a bug. It's probably my fault; sorry about that. (I wrote all of the lambda-to-delegate conversion checking code.)

I'm in a coffee shop right now and I don't have access to the compiler sources from here. I'll try to find some time to reproduce this in the debug build tomorrow and see if I can work out what's going on. If I don't find the time, I'll be out of the office until after Christmas.

Your observation that introducing a variable of type Action causes the problem to disappear is extremely interesting. The compiler maintains many caches for both performance reasons and for analysis required by the language specification. Lambdas and local variables in particular have lots of complex caching logic. I'd be willing to bet as much as a dollar that some cache is being initialized or filled in wrong here, and that the use of the local variable fills in the right value in the cache.

Thanks for the report!

UPDATE: I am now on the bus and it just came to me; I think I know exactly what is wrong. The compiler is lazy, particularly when dealing with types that came from metadata. The reason is that there could be hundreds of thousands of types in the referenced assemblies and there is no need to load information about all of them. You're going to use far less than 1% of them probably, so let's not waste a lot of time and memory loading stuff you're never going to use. In fact the laziness goes deeper than that; a type passes through several "stages" before it can be used. First its name is known, then its base type, then whether its base type hierarchy is well-founded (acyclic, etc), then its type parameter constraints, then its members, then whether the members are well-founded (that overrides override something of the same signature, and so on.) I'll bet that the conversion logic is failing to call the method that says "make sure the types of all the delegate parameters have their members known", before it checks the signature of the delegate invoke for compatibility. But the code that makes a local variable probably does do that. I think that during the conversion checking, the Action type might not even have an invoke method as far as the compiler is concerned.

We'll find out shortly.

UPDATE: My psychic powers are strong this morning. When overload resolution attempts to determine if there is an "Invoke" method of the delegate type that takes zero arguments, it finds zero Invoke methods to choose from. We should be ensuring that the delegate type metadata is fully loaded before we do overload resolution. How strange that this has gone unnoticed this long; it repros in C# 3.0. Of course it does not repro in C# 2.0 simply because there were no lambdas; anonymous methods in C# 2.0 require you to state the type explicitly, which creates a local, which we know loads the metadata. But I would imagine that the root cause of the bug - that overload resolution does not force loading metadata for the invoke - goes back to C# 1.0.

Anyway, fascinating bug, thanks for the report. Obviously you've got a workaround. I'll have QA track it from here and we'll try to get it fixed for C# 5. (We have missed the window for Service Pack 1, which is already in beta.)

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Don't worry about it, it was fun searching for what exactly caused the bug to happen, and Femaref's answer provides an excellent workaround for the problem. I'd love to hear it if you find out anything from the compiler sources :) – Alex ten Brink Dec 17 '10 at 13:23
  • It looks like that's indeed the problem: if I add 'Action x;' before the line that gives the error, the error disappears, but adding 'Func x' does not. – Alex ten Brink Dec 17 '10 at 15:41
  • 8
    I think it's not that strange it went unnoticed for so long: it only fails if you define a *delegate* that takes another *delegate*, then proceed to use it in a *different project* as a *lambda*, *without ever referring* to the parameter delegate type in the code preceding the lambda. If I replace the declaring type by 'Action' or even 'Action>', it gives no error. – Alex ten Brink Dec 17 '10 at 16:16
24

This probably is a problem with type inference, apperently the compiler infers a as an Action<T> instead of Action (it might think a is ActionSurrogate, which would fit the Action<Action>> signature). Try specifying the type of a explicitly:

    ActionSurrogate b = (Action a) =>
                        {
                            a();
                        };

If this is not the case - might check around your project for any self defined Action delegates taking one parameter.

InteXX
  • 6,135
  • 6
  • 43
  • 80
Femaref
  • 60,705
  • 7
  • 138
  • 176
  • That seems unlikely, but it's the first thing I'd try. – Gabe Dec 17 '10 at 01:16
  • 1
    That fixes the problem. I didn't think of explicitly naming the type in the lambda. I guess it's a type inference fail then. Oddly enough, by using a lambda in the code that precedes the definition, the error does not occur... – Alex ten Brink Dec 17 '10 at 01:43
  • I don't think it's a type inference failure, because VS shows me perfect prompts that `a` is an `Action`, even I didn't explicitly naming the type. – Cheng Chen Dec 17 '10 at 03:03
  • 1
    @Danny: just because Visual Studio shows it that way doesn't mean the compiler compiles it correctly... See Eric's post for the apparent reason. – Femaref Dec 17 '10 at 09:27
2
    public static void ThisWontCompile()
        {
            ActionSurrogate b = (Action a) =>
            {
                a();
            };


        }

This will compile. Some glitch with the compiler its unable to find the Action delegate without parameters. That's why you are getting the error.

public delegate void Action();
public delegate void Action<T>();
public delegate void Action<T1,T2>();
public delegate void Action<T1,T2,T3>();
public delegate void Action<T1,T2,T3,T4>();
Amit Bagga
  • 648
  • 3
  • 11