C# 3.0 generic type inference - passing a delegate as a function parameter
Asked Answered
D

5

25

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:

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?

Dominations answered 2/1, 2009 at 20:50 Comment(0)
P
18

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.

Procreate answered 2/1, 2009 at 21:0 Comment(4)
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?Marozas
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.Dominations
@AndrewHare: Although there are a few situations were a parameter T could be inferred from a method group, in most cases in cannot. For example, given void XX(object s); Action<T> YY<T>(Action<T> a); ...var qq=YY(XX);, what types could qq be? Although it would seem most natural for it to be an Action<object>, XX could be assigned to an Action<T> for any class type T inherited from object. Allowing type inference only in those cases where it's ambiguous would occasionally be helpful, but the rules for when it is or isn't allowed would likely be confusing,.Denims
Just ran into a similar problem (using user-defined types/interfaces rather than "Action"), and found this answer via one of the duplicates... looks like I'll need to decide between a redesign or lots of ugly casts / explicit type arguments from calling sites. Thanks for your clear explanation.Dhar
B
8

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>.

Berkey answered 2/1, 2009 at 21:5 Comment(11)
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).Marozas
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.Dominations
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..Marozas
@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.Berkey
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.Marozas
@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.Berkey
Also, I wanted to note that using the line: bar(x => foo(x)); is a good workaround.Berkey
bar(x => foo(x)); is still ambiguous in my code. Compiler no likey.Procreate
Sorry, you're right. You can use bar<int>(foo) though, right?Berkey
@Berkey - Yes, ambiguity is a potential option but it is not an option currently. Why should the compiler break on a potential ambiguity?Dominations
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.Berkey
H
6

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.

Harmattan answered 2/1, 2009 at 21:2 Comment(3)
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...Marozas
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 [...]"Harmattan
Hm, ok, guess I need to turn my head another turn around to wrap it around this C# spec :)Marozas
A
5

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.

Agustinaah answered 2/1, 2009 at 22:27 Comment(0)
C
0

Just for completeness, this is not specific to C#: The same VB.NET code fails similarly:

Imports System

Module Test
  Sub foo(ByVal x As integer)
  End Sub
  Sub bar(Of T)(ByVal f As Action(Of T))
  End Sub

  Sub Main()
    Dim f As Action(Of integer) = AddressOf foo ' I can do this
    bar(f) ' and then do this
    bar(AddressOf foo) ' but this does not work
  End Sub
End Module

error BC32050: Type parameter 'T' for 'Public Sub bar(Of T)(f As System.Action(Of T))' cannot be inferred.

Cranio answered 4/7, 2011 at 14:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.