-->

Delegate as first param to an Extension Method

2020-07-06 06:02发布

问题:

Ladies and Gents,

I recently tried this experiment:

static class TryParseExtensions
{
    public delegate bool TryParseMethod<T>(string s, out T maybeValue);
    public static T? OrNull<T>(this TryParseMethod<T> tryParser, string s) where T:struct 
    {
        T result;
        return tryParser(s, out result) ? (T?)result : null;
    }
}

// compiler error "'int.TryParse(string, out int)' is a 'method', which is not valid in the given context"
var result = int.TryParse.OrNull("1");  // int.TryParse.OrNull<int>("1"); doesnt work either

// compiler error: type cannot be infered....why?
var result2 = TryParseExtensions.OrNull(int.TryParse, "2"); 

// works as expected
var result3 = TryParseExtensions.OrNull<int>(int.TryParse, "3");
      var result4 = ((TryParseExtensions.TryParseMethod<int>)int.TryParse).OrNull("4");

I am wondering two things:

  • Why can the compiler not infer the "int" type parameter?

  • Do I understand correctly that extensions methods do not get discovered on Delegate types, as I guess they arent really of that type (but are a "Method") that only happen to match the delegates signature? As such a cast solves this. Would it be infeasable to enable scenario 1 to work (not this one specifically of course, but in general)? I guess from a language/compiler perspective and would it actually be useful, or am I just (attempting to) wildly abusing things here?

Looking forward to some insights. Thnx

回答1:

You have a number of questions here. (In the future I would recommend that when you have multiple questions, split them up into multiple questions rather than one posting with several questions in it; you'll probably get better responses.)

Why can the compiler not infer the "int" type parameter in:

TryParseExtensions.OrNull(int.TryParse, "2");  

Good question. Rather than answer that here, I refer you to my 2007 article which explains why this did not work in C# 3.0:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/05/c-3-0-return-type-inference-does-not-work-on-member-groups.aspx

Summing up: fundamentally there is a chicken-and-egg problem here. We must do overload resolution on int.TryParse to determine which overload of TryParse is the intended one (or, if none of them work, what the error is.) Overload resolution always tries to infer from arguments. In this case though, it is precisely the type of the argument that we are attempting to infer.

We could come up with a new overload resolution algorithm that says "well, if there's only one method in the method group then pick that one even if we don't know what the arguments are", but that seems weak. It seems like a bad idea to special-case method groups that have only one method in them because that then penalizes you for adding new overloads; it can suddenly be a breaking change.

As you can see from the comments to that article, we got a lot of good feedback on it. The best feedback was got was basically "well, suppose type inference has already worked out the types of all the argument and it is the return type that we are attempting to infer; in that case you could do overload resolution". That analysis is correct, and changes to that effect went into C# 4. I talked about that a bit more here:

http://blogs.msdn.com/b/ericlippert/archive/2008/05/28/method-type-inference-changes-part-zero.aspx

Do I understand correctly that extensions methods do not get discovered on delegate types, as I guess they arent really of that type (but are a "Method") that only happen to match the delegates signature?

Your terminology is a bit off, but your idea is correct. We do not discover extension methods when the "receiver" is a method group. More generally, we do not discover extension methods when the receiver is something that lacks its own type, but rather takes on a type based on its context: method groups, lambdas, anonymous methods and the null literal all have this property. It would be really bizarre to say null.Whatever() and have that call an extension method on String, or even weirder, (x=>x+1).Whatever() and have that call an extension method on Func<int, int>.

The line of the spec which describes this behaviour is :

An implicit identity, reference or boxing conversion [must exist] from [the receiver expression] to the type of the first parameter [...].

Conversions on method groups are not identity, reference or boxing conversions; they are method group conversions.

Would it be infeasable to enable scenario 1 to work (not this one specifically of course, but in general)? I guess from a language/compiler perspective and would it actually be useful, or am I just (attempting to) wildly abusing things here?

It is not infeasible. We've got a pretty smart team here and there's no theoretical reason why it is impossible to do so. It just doesn't seem to us like a feature that adds more value to the language than the cost of the additional complexity.

There are times when it would be useful. For example, I'd like to be able to do this; suppose I have a static Func<A, R> Memoize<A, R>(this Func<A, R> f) {...}:

var fib = (n=>n<2?1:fib(n-1)+fib(n-2)).Memoize();

Instead of what you have to write today, which is:

Func<int, int> fib = null;
fib = n=>n<2?1:fib(n-1)+fib(n-2);
fib = fib.Memoize();

But frankly, the additional complexity the proposed feature adds to the language is not paid for by the small benefit in making the code above less verbose.



回答2:

The reason for the first error:
int.TryParse is a method group, not an object instance of any type. Extension methods can only be called on object instances. That's the same reason why the following code is invalid:

var s = int.TryParse;

This is also the reason why the type can't be inferred in the second example: int.TryParse is a method group and not of type TryParseMethod<int>.

I suggest, you use approach three and shorten the name of that extension class. I don't think there is any better way to do it.



回答3:

Note that your code works if you first declare :

TryParseExtensions.TryParseMethod<int> tryParser = int.TryParse;

and then use tryParser where you used int.TryParse.

The problem is that the compiler doesn't know which overload of int.Parse you're speaking about. So it cannot completely infer it : are you speaking about TryParse(String, Int32) or TryParse(String, NumberStyles, IFormatProvider, Int32) ? The compiler can't guess and won't arbitrarily decide for you (fortunately !).

But your delegate type makes clear which overload you're interested in. That's why assigning tryParser is not a problem. You're not speaking anymore of a "method group" but of a well identified method signature inside this group of methods called int.TryParse.