无法从用法中推断出C#通用方法类型参数

时间:2018-08-17 18:03:17

标签: c# generics methods interface type-inference

最近,我尝试了一种访问者模式的实现,在该模式中,我尝试使用通用接口强制实施Accept&Visit方法:

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

-其目的是:1)将某些类型“ Foo”标记为可被此类访问者访问,这反过来又是“此类Foo的访问者”,以及2)在实现的可访问类型上实施正确签名的Accept方法,就像这样:

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

到目前为止,访问者界面还不错:

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

-应该1)将访问者标记为“可以访问”可访问电视对象2)此可访问对象的结果类型(TResult)应该是什么3)强制每个访问者都使用正确签名的访问方法,访问者实现是“可访问的”参观”,就像这样:

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

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

非常愉快和漂亮,这让我写道:

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

很好。

现在,当我添加另一个可访问类型时,悲伤的时刻开始了,例如:

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

可以被CountVisitor访问的

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

突然中断了Accept方法中的类型推断! (这会破坏整个设计)

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

给我:

  

“无法从用法中推断出方法'Foo.Accept<TResult>(IVisitor<TResult, Foo>)'的类型参数。”

有人可以详细说明为什么吗? IVisitor<T, Foo>实现的CountVisitor接口只有一个版本-或者,如果由于某种原因无法删除IVisitor<T, Bar>,则它们都具有相同的T -int,=在那里没有其他类型的功能。一旦有不止一个合适的候选者,类型推断就会放弃吗? (有趣的事实:ReSharper认为int中的theFoo.Accept<int>(...)是多余的:P,即使没有它也无法编译)

3 个答案:

答案 0 :(得分:13)

似乎类型推断以一种贪婪的方式工作,首先尝试匹配 method 泛型,然后再匹配类的泛型。因此,如果您说

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

它起作用了,这很奇怪,因为Foo是类通用类型的唯一候选对象。

首先,如果将方法通用类型替换为第二类通用类型,则它会起作用:

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());
    }
}

第二(这是一个奇怪的部分,突出了类型推断的工作原理),如果您在int访问者中将string替换为Bar,会发生什么情况:

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

首先,您会遇到相同的错误,但请注意如果强制输入字符串会发生什么情况:

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

错误CS1503:参数1:无法从'CountVisitor'转换为   'IVisitor<string, Foo>'

这建议编译器首先查看方法泛型类型(在您的情况下为TResult),并且在找到更多候选者时立即失败。甚至没有进一步研究类通用类型。

我试图从Microsoft找到类型推断规范,但找不到任何内容。

答案 1 :(得分:9)

  

只要有不止一个合适的候选者,类型推断就会放弃吗?

是的,在这种情况下是的。尝试推断方法的通用类型参数(TResult)时,类型推断算法似乎在CountVisitor上对类型IVisitor<TResult, TVisitable>有两个推断的失败。


C# 5 specification(我能找到的最新版本)中,第7.5.2节:

  

Tr M<X1…Xn>(T1 x1 … Tm xm)

     

使用M(E1 …Em)形式的方法调用,类型推断的任务是查找唯一的类型参数   为每个类型参数S1…Sn使用X1…Xn,这样调用M<S1…Sn>(E1…Em)才有效。

编译器采取的第一步如下(第7.5.2.1节):

  

对于每个方法参数Ei

     
      
  • 如果Ei是匿名函数,则根据Ei进行显式参数类型推断(§7.5.2.7)。   到Ti

  •   
  • 否则,如果Ei的类型为U,而xi是一个值参数,则进行下界推断 U Ti

  •   

您只有一个参数,因此我们仅有的Ei是表达式new CountVisitor()。显然这不是一个匿名函数,因此我们处于第二个要点。显而易见,在我们的案例中,U的类型为CountVisitor。 “ xi是一个值参数”位基本上意味着它不是outinref等变量,在这里就是这种情况。

在这一点上,我们现在需要从CountVisitorIVisitor<TResult, TVisitable>进行下界推断,第7.5.2.9节的相关部分(由于变量开关,我们有V = IVisitor<TResult, TVisitable>):

  
      
  • 否则,通过检查以下任一情况是否适用来确定集合U1…UkV1…Vk:      
        
    • V是数组类型V1[…],而U是数组类型U1[…](或有效基本类型为U1[…]的类型参数)同等级
    •   
    • VIEnumerable<V1>ICollection<V1>IList<V1>之一,而U是一维数组类型U1[](或一种类型有效基本类型为U1[])的参数
    •   
    • V是构造的类,结构,接口或委托类型C<V1…Vk>,并且存在唯一类型C<U1…Uk>,使得U(或者,如果{{1} }是类型参数,其有效基类或其有效接口集的任何成员都与U相同,继承(直接或间接)或实现(直接或间接)C<U1…Uk>
    •   
  •   
     

(“唯一性”限制意味着在接口C<T>{} class U: C<X>, C<Y>{}中,从UC<T>进行推断时不会进行推断,因为U1可能是{{ 1}}或X。)

我们可以跳过前两种情况,因为它们显然不适用,而第三种情况就是我们属于的情况。编译器尝试找到Y实现的唯一类型C<U1…Uk>,并找到两种这样的类型CountVisitor和{{1} }。请注意,规范给出的示例与您的示例几乎相同。

由于唯一性约束,对此方法参数不作任何推断。由于编译器无法从参数中推断出任何类型信息,因此无需继续尝试推断IVisitor<int, Foo>并因此失败。


关于为什么存在唯一性约束,我的猜测是它简化了算法,从而简化了编译器的实现。如果您有兴趣,请here's a link作为源代码,在其中Roslyn(现代C#编译器)实现通用方法类型推断。

答案 2 :(得分:5)

在C#中,您可以通过使用dynamic关键字来删除“双重分发”来简化“访客”模式。

您可以这样实现访客:

public class CountVisitor : IVisitor<int, IVisitable>
{
   public int Visit( IVisitable v )
   {
       dynamic d = v;
       Visit(d);
   }

    private int Visit( Foo f ) 
    {
        return 42;
    }

    private int Visit( Bar b )
    {
        return 7;
    }
}

通过这样做,尽管FooBar仍必须实现Visitor的公共接口才能正常运行,但您无需在{{1}}和{{1}}上实现Accept方法。 / p>