Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# generic method type argument not inferred from usage

Tags:

Recently I've experimented with an implementation of the visitor pattern, where I've tried to enforce Accept & Visit methods with generic interfaces:

public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable> {     TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor); } 

-whose purpose is to 1) mark certain type "Foo" as visitable by such a visitor, which in turn is a "visitor of such type Foo" and 2) enforce Accept method of the correct signature on the implementing visitable type, like so:

public class Foo : IVisitable<Foo> {     public TResult Accept<TResult>(IVisitor<TResult, Foo> visitor) => visitor.Visit(this); } 

So far so good, the visitor interface:

public interface IVisitor<out TResult, in TVisitable> where TVisitable : IVisitable<TVisitable> {     TResult Visit(TVisitable visitable); } 

-should 1) mark the visitor as "able to visit" the TVisitable 2) what the result type (TResult) for this TVisitable should be 3) enforce Visit method of a correct signature per each TVisitable the visitor implementation is "able to visit", like so:

public class CountVisitor : IVisitor<int, Foo> {     public int Visit(Foo visitable) => 42; }  public class NameVisitor : IVisitor<string, Foo> {     public string Visit(Foo visitable) => "Chewie"; } 

Quite pleasantly & beautifully, this lets me write:

var theFoo = new Foo(); int count = theFoo.Accept(new CountVisitor()); string name = theFoo.Accept(new NameVisitor()); 

Very good.

Now the sad times begin, when I add another visitable type, like:

public class Bar : IVisitable<Bar> {     public TResult Accept<TResult>(IVisitor<TResult, Bar> visitor) => visitor.Visit(this); } 

which is visitable by let's say just the CountVisitor:

public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar> {     public int Visit(Foo visitable) => 42;     public int Visit(Bar visitable) => 7; } 

which suddenly breaks the type inference in the Accept method! (this destroys the whole design)

var theFoo = new Foo(); int count = theFoo.Accept(new CountVisitor()); 

giving me:

"The type arguments for method 'Foo.Accept<TResult>(IVisitor<TResult, Foo>)' cannot be inferred from the usage."

Could anyone please elaborate on why is that? There is only one version of IVisitor<T, Foo> interface which the CountVisitor implements - or, if the IVisitor<T, Bar> can't be eliminated for some reason, both of them have the same T - int, = no other type would work there anyway. Does the type inference give up as soon as there are more than just one suitable candidate? (Fun fact: ReSharper thinks the int in theFoo.Accept<int>(...) is redundant :P, even though it wouldn't compile without it)

like image 565
Vladi Pavelka Avatar asked Aug 17 '18 18:08

Vladi Pavelka


1 Answers

It seems that the type inference works in a greedy way, first trying to match the method generic types, then the class generic types. So if you say

int count = theFoo.Accept<int>(new CountVisitor()); 

it works, which is strange, since Foo is the only candidate for the class generic type.

First, if you replace the method generic type with a second class generic type, it works:

public interface IVisitable<R, out T> where T: IVisitable<int, T> {     R Accept(IVisitor<R, T> visitor); }  public class Foo : IVisitable<int, Foo> {     public int Accept(IVisitor<int, Foo> visitor) => visitor.Visit(this); }  public class Bar : IVisitable<int, Bar> {     public int Accept(IVisitor<int, Bar> visitor) => visitor.Visit(this); }  public interface IVisitor<out TResult, in T> where T: IVisitable<int, T> {     TResult Visit(T visitable); }  public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar> {     public int Visit(Foo visitable) => 42;     public int Visit(Bar visitable) => 7; }  class Program {     static void Main(string[] args) {         var theFoo = new Foo();         int count = theFoo.Accept(new CountVisitor());     } } 

Second (and this is the strange part which highlights how the type inference works) look what happens if you replace int with string in the Bar visitor:

public class CountVisitor : IVisitor<int, Foo> , IVisitor<string, Bar> {     public int Visit(Foo visitable) => 42;     public string Visit(Bar visitable) => "42"; } 

First, you get the same error, but watch what happens if you force a string:

    int count = theFoo.Accept<string>(new CountVisitor()); 

error CS1503: Argument 1: cannot convert from 'CountVisitor' to 'IVisitor<string, Foo>'

Which suggests that the compiler first looks at the method generic types (TResult in your case) and fails immediately if it finds more candidates. It doesn't even look further, at the class generic types.

I tried to find a type inference specification from Microsoft, but couldn't find any.

like image 108
memo Avatar answered Oct 20 '22 07:10

memo