views:

4425

answers:

4

I am wondering why the C# 3.0 compiler is unable to infer the type of a method when it is passed as a parameter to a generic function when it can implicitly create a delegate for the same method.

Here is an example:

using System;

class Test
{
    static void foo(int x) { }
    static void bar<T>(Action<T> f) { }

    static void test()
    {
     Action<int> f = foo; // I can do this
     bar(f); // and then do this
     bar(foo); // but this does not work
    } 
}

I would have thought that I would be able to pass foo to bar and have the compiler infer the type of Action<T> from the signature of the function being passed but this does not work. However I can create an Action<int> from foo without casting so is there a legitimate reason that the compiler could not also do the same thing via type inference?

+10  A: 

Maybe this will make it clearer:

public class SomeClass
{
    static void foo(int x) { }
    static void foo(string s) { }
    static void bar<T>(Action<T> f){}
    static void barz(Action<int> f) { }
    static void test()
    {
        Action<int> f = foo;
        bar(f);
        barz(foo);
        bar(foo);
        //these help the compiler to know which types to use
        bar<int>(foo);
        bar( (int i) => foo(i));
    }
}

foo is not an action - foo is a method group.

  • In the assignment statement, the compiler can tell clearly which foo you're talking about, since the int type is specified.
  • In the barz(foo) statement, the compiler can tell which foo you're talking about, since the int type is specified.
  • In the bar(foo) statement, it could be any foo with a single parameter - so the compiler gives up.

Edit: I've added two (more) ways to help the compiler figure out the type (ie - how to skip the inference steps).

From my reading of the article in JSkeet's answer, the decision to not infer the type seems to be based on a mutual infering scenario, such as

  static void foo<T>(T x) { }
  static void bar<T>(Action<T> f) { }
  static void test()
  {
    bar(foo); //wut's T?
  }

Since the general problem was unsolve-able, they choose to left specific problems where a solution exists as unsolved.

As a consequence of this decision, you won't be adding a overload for a method and getting a whole lot of type confusion from all the callers that are used to a single member method group. I guess that's a good thing.

David B
So the compiler gives up even in the case when only one matching method is found? Is this perhaps one of those things they added to make it less probably that a future change to the code changes the meaning of existing code involuntarily?
Lasse V. Karlsen
This is close to what I am looking for but in my example the method group has only one method so I would have thought that the compiler would know to infer the delegate type from that method. In your example the compiler is obviously confused because there are multiple foo functions.
Andrew Hare
+3  A: 

That is slightly odd, yes. The C# 3.0 spec for type inference is hard to read and has mistakes in it, but it looks like it should work. In the first phase (section 7.4.2.1) I believe there's a mistake - it shouldn't mention method groups in the first bullet (as they're not covered by explicit parameter type inference (7.4.2.7) - which means it should use output type inference (7.4.2.6). That looks like it should work - but obviously it doesn't :(

I know that MS is looking to improve the spec for type inference, so it might become a little clearer. I also know that regardless of the difficulty of reading it, there are restrictions on method groups and type inference - restrictions which could be special-cased when the method group is only actually a single method, admittedly.

Eric Lippert has a blog entry on return type inference not working with method groups which is similar to this case - but here we're not interested in the return type, only on the parameter type. It's possible that other posts in his type inference series may help though.

Jon Skeet
In the spec, it mentions chapter 7.5.5.1 on method invocations, where it says that a method is compatible if it has the same number of generic arguments, and the arguments matches the type of the method parameters, it doesn't say anything about deducing matching generic argument types...
Lasse V. Karlsen
In 7.5.5.1 there's this: "o If F is generic and M has no type argument list, F is a candidate when: Type inference (§7.4.2) succeeds, inferring a list of type arguments for the call [...]"
Jon Skeet
Hm, ok, guess I need to turn my head another turn around to wrap it around this C# spec :)
Lasse V. Karlsen
+5  A: 

The reasoning is that if the type ever expands there should be no possibility of failure. i.e., if a method foo(string) is added to the type, it should never matter to existing code - as long as the contents of existing methods don't change.

For that reason, even when there is only one method foo, a reference to foo (known as a method group) cannot be cast to a non-type-specific delegate, such as Action<T> but only to a type-specific delegate such as Action<int>.

configurator
How does other changes factor into this? Is this a rule or a guideline somewhere? I can see some obvious issues with other changes, like foo(float i) { ... }, then foo(10), then add foo(int i).
Lasse V. Karlsen
This is a good point - I didn't think of that. However, in the case that I did add a new foo later then the compiler (IMHO) should break and complain about the ambiguity then when there is actually a problem. Right now when there are no other foo's it seems like it ought to work.
Andrew Hare
As it is now, it changes the existing method to call foo(int i) instead of implicitly converting the integer literal to a float value and calling the foo(float i) as it did originally. There's many such changes, but I think they've gotten better at blocking them, perhaps that's the reason for this..
Lasse V. Karlsen
@lassevk - what I said is a simplification, and is only in regard to non-matching types i.e. string vs int vs SomeOtherType, not types which would cause an implicit conversion or more-specific types that would cause polymorphism to kick in.
configurator
ok, just wondering about it :) but I have noticed the new syntax of the compiler has gotten more checks like this, but perhaps that's just coincidental.
Lasse V. Karlsen
@Andrew: since ambiguity is an option as in my example, and in some slightly more complex cases even infinite loops can occur, the compiler is set to never infer types on method groups. Indeed, Jon Skeet's link to Eric Lippert's blog could be quite useful if you want a deeper understanding of this.
configurator
Also, I wanted to note that using the line: bar(x => foo(x)); is a good workaround.
configurator
bar(x => foo(x)); is still ambiguous in my code. Compiler no likey.
David B
Sorry, you're right. You can use bar<int>(foo) though, right?
configurator
@configurator - Yes, ambiguity is a potential option but it is not an option currently. Why should the compiler break on a potential ambiguity?
Andrew Hare
The C# designers are not keen to put in language features that might work and might not - it might be ok, but it might be ambiguous. Also, I believe the algorithm for determining whether or not it actually is ambiguous is far more complex than we imagine. So this kind of inference is never allowed.
configurator
bar<int>(foo) works great.
David B
+2  A: 

Keep in mind that the assignment

Action<int> f = foo;

already has lots of syntactic sugar. The compiler actually generates code for this statement:

Action<int> f = new Action<int>(foo);

The corresponding method call compiles without problem:

bar(new Action<int>(foo));

Fwiw, so does helping the compiler to deduce the type argument:

bar<int>(foo);

So it boils down to the question, why the sugar in the assignment statement but not in the method call? I'd have to guess that it's because the sugar is unambiguous in the assignment, there is only one possible substitution. But in the case of method calls, the compiler writers already had to deal with the overload resolution problem. The rules of which are quite elaborate. They probably just didn't get around to it.

Hans Passant
+1 for bar<int>(foo), I didn't think of that
Andrew Hare