Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler not resolving to expected extension method

I noticed today when trying to convert an inline lambda function to a closure so I could use the same lambda in multiple places. This will compile to the correct extension method:

appBuilder.Use((ctx, next) => {
    Console.WriteLine("Test");
    return next();
});

That Use is an extension defined by:

public static IAppBuilder Use(this IAppBuilder app, Func<IOwinContext, Func<Task>, Task> handler);

Now if I do the same thing, but move the inline to a variable:

Func<IOwinContext, Func<Task>, Task> handler = (ctx, next) => {
        Console.WriteLine("Test");
        return next();
    };
appBuilder.Use(handler);

The compiler resolves to this method (not the extension):

IAppBuilder Use(object middleware, params object[] args);

What am I doing here to cause that method to change signatures?

Thanks in advance.

like image 689
daniefer Avatar asked Dec 02 '22 10:12

daniefer


2 Answers

What am I doing here to cause that method to change signatures?

A lambda expression doesn't have a type, and is only convertible to compatible delegate and expression-tree types.

Therefore the regular IAppBuilder method with (object, params object[]) parameters is not applicable for your call with the lambda expression argument. At that point, the compiler will look for extension methods.

Compare that with the version with the handler variable - at that point, you've got an argument which is convertible to object, and it's fine for a parameter array to have no values... so the regular method is applicable.

Importantly, if the compiler finds any applicable non-extension methods, it performs overload resolution using those. Extension methods are only used when no non-extension methods are applicable.

If you had either two extension methods or two regular methods, overload resolution would determine that the one with the more specific parameter is better for this invocation than the (object, params object[]) one... but that's not the case; the two are never compared.

like image 88
Jon Skeet Avatar answered Dec 04 '22 14:12

Jon Skeet


Lambdas are very unusual in C# in that they're one of very few types of expressions that don't actually have a type. They rely on their context in order to know what their type is. (Another example being null.)

You can take the exact same lambda, and change the code surrounding it, and it will change the type that that lambda resolves to. If you put it somewhere expecting an Expression<T>, it'll resolve to an expression, if you put it somewhere expecting a delegate, it'll end up being a delegate (with the two being radically different from each other), and the type of delegate it's bound to is based on the type of delegate expected, so the same lambda can resolve to entirely different types of delegates based on what's expected.

Almost nothing else acts like this; most expressions have exactly one type that they resolve to, and you can determine what that type is without looking at the context at all.

So in your first example, when you have:

(ctx, next) => {
    Console.WriteLine("Test");
    return next();
}

You can't determine the type alone. You need to look at where that expression is used to try to find something that that lambda can be converted to. That lambda can't be converted to an object. The compiler would have no idea if it's supposed to be an expression or a delegate, or which delegate it's supposed to be. But it can convert it to an Func<IOwinContext, Func<Task>, Task>, because it's a delegate of an appropriate type. So that's the overload used.

When you pass in handler, that's not a lambda, it's just an instance of a delegate, and that, like any other object, can be converted to object, so the other overload is valid, and being an instance method it "wins" when both are valid.

like image 27
Servy Avatar answered Dec 04 '22 15:12

Servy