C#泛型方法类型参数不是从用法推断出来的

Vla*_*lka 24 c# generics methods type-inference interface

最近我尝试了访问者模式的实现,我尝试使用通用接口强制执行A​​ccept&Visit方法:

public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable>
{
    TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor);
}
Run Code Online (Sandbox Code Playgroud)

- 其目的是1)将某些类型的"Foo"标记为可由这样的访问者访问,而访问者又是"这种类型Foo的访问者",2)在实现访问类型上强制执行正确签名的Accept方法,如此:

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

到目前为止,访客界面非常好:

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

- 1)标记访问者"能够访问"TVisitable 2)该TVisitable的结果类型(TResult)应该是3)强制执行访问方法每个TVisitable访问者实现"能够访问"的正确签名,像这样:

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

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

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

var theFoo = new Foo();
int count = theFoo.Accept(new CountVisitor());
string name = theFoo.Accept(new NameVisitor());
Run Code Online (Sandbox Code Playgroud)

很好.

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

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

可以通过以下方式来访问CountVisitor:

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

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

var theFoo = new Foo();
int count = theFoo.Accept(new CountVisitor());
Run Code Online (Sandbox Code Playgroud)

给我:

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

任何人都可以详细说明为什么会这样?实现只有一个版本的IVisitor<T, Foo>接口CountVisitor- 或者,如果由于IVisitor<T, Bar>某种原因无法消除,它们都具有相同的T- int,=无论如何其他类型都不会工作.只要有一个合适的候选人,类型推断是否会立即放弃?(有趣的事实:ReSharper认为intin theFoo.Accept<int>(...)是多余的:P,即使没有它也不会编译)

mem*_*emo 13

似乎类型推断以贪婪的方式工作,首先尝试匹配方法泛型类型,然后是类泛型类型.所以,如果你说

int count = theFoo.Accept<int>(new CountVisitor());
Run Code Online (Sandbox Code Playgroud)

它很有用,这很奇怪,因为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());
    }
}
Run Code Online (Sandbox Code Playgroud)

其次(这是其中突出的类型推断是如何工作的怪一部分)看,如果你更换发生什么intstringBar访客:

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

首先,您得到相同的错误,但是看看如果强制使用字符串会发生什么:

    int count = theFoo.Accept<string>(new CountVisitor());
Run Code Online (Sandbox Code Playgroud)

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

这表明编译器首先查看方法泛型类型(TResult在您的情况下)并且如果找到更多候选者则立即失败.在类泛型类型中它甚至没有进一步看.

我试图从微软找到一个类型推断规范,但找不到任何.


DPe*_*er1 9

只要有一个合适的候选人,类型推断是否会立即放弃?

是的,在这种情况下确实如此.在尝试推断方法的泛型类型参数(TResult)时,类型推断算法似乎在CountVisitor对类型进行两次推断时失败IVisitor<TResult, TVisitable>.


C#5规范(我能找到的最新规范),§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是匿名函数,则从中 生成显式参数类型推断(第7.5.2.7节)EiTi

  • 否则,如果Ei有一个类型Uxi是一个值参数然后一个下界推断是由 U Ti.

你只有一个参数,所以我们只有Ei表达式new CountVisitor().它显然不是一个匿名函数,所以我们处于第二个要点.在我们的例子中,看到它U是类型的,这是微不足道的CountVisitor.的" xi是一个值参数"位基本上意味着它不是一个out,in,ref等变量,它是这里的情况.

在这一点上,我们现在需要做一个下限推断来自CountVisitorIVisitor<TResult, TVisitable>§7.5.2.9(其中因变量开关,我们的相关部分V= IVisitor<TResult, TVisitable>在我们的例子):

  • 否则,设置U1…UkV1…Vk通过检查是否适用以下任何一种情况来确定:
    • V是一个数组类型V1[…],U是一个相同排名的数组类型U1[…](或有效基类型的类型参数U1[…])
    • V是一个IEnumerable<V1>,ICollection<V1>IList<V1>U是一维阵列型U1[](或者类型参数,其有效碱型是U1[])
    • V是一个构造的类,结构,接口或委托类型,C<V1…Vk>并且有一个唯一的类型C<U1…Uk>,使得U(或者,如果U是类型参数,它的有效基类或其有效接口集的任何成员)是相同的,继承自(直接或(间接)或实施(直接或间接)C<U1…Uk>.

("唯一性"限制意味着在案例界面中C<T>{} class U: C<X>, C<Y>{},在推断时不会进行推断U,C<T>因为U1可能是XY.)

我们可以跳过前两个案例,因为它们显然不适用,第三个案例是我们陷入的案例.编译器尝试找到一个独特的类型C<U1…Uk>CountVisitor实现了,发现2层这样的类型,IVisitor<int, Foo>IVisitor<int, Bar>.请注意,规范给出的示例几乎与您的示例相同.

由于唯一性约束,因此不对此方法参数进行推断.由于编译器无法从参数中推断出任何类型信息TResult,因此无法继续尝试推断并因此失败.


至于为什么存在唯一性约束,我的猜测是它简化了算法并因此简化了编译器实现.如果您感兴趣,这里是源代码的链接,其中Roslyn(现代C#编译器)实现了泛型方法类型推断.


Fre*_*els 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

通过这样做,您不需要实现Accept方法Foo,Bar尽管它们仍然必须Visitor为工作场所实现一个通用接口.