Vla*_*lka 24 c# generics methods type-inference interface
最近我尝试了访问者模式的实现,我尝试使用通用接口强制执行Accept&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)
其次(这是其中突出的类型推断是如何工作的怪一部分)看,如果你更换发生什么int用string的Bar访客:
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在您的情况下)并且如果找到更多候选者则立即失败.在类泛型类型中它甚至没有进一步看.
我试图从微软找到一个类型推断规范,但找不到任何.
只要有一个合适的候选人,类型推断是否会立即放弃?
是的,在这种情况下确实如此.在尝试推断方法的泛型类型参数(TResult)时,类型推断算法似乎在CountVisitor对类型进行两次推断时失败IVisitor<TResult, TVisitable>.
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有一个类型U和xi是一个值参数然后一个下界推断是由从U到Ti.
你只有一个参数,所以我们只有Ei表达式new CountVisitor().它显然不是一个匿名函数,所以我们处于第二个要点.在我们的例子中,看到它U是类型的,这是微不足道的CountVisitor.的" xi是一个值参数"位基本上意味着它不是一个out,in,ref等变量,它是这里的情况.
在这一点上,我们现在需要做一个下限推断来自CountVisitor于 IVisitor<TResult, TVisitable>§7.5.2.9(其中因变量开关,我们的相关部分V= IVisitor<TResult, TVisitable>在我们的例子):
- 否则,设置
U1…Uk并V1…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可能是X或Y.)
我们可以跳过前两个案例,因为它们显然不适用,第三个案例是我们陷入的案例.编译器尝试找到一个独特的类型C<U1…Uk>是CountVisitor实现了,发现2层这样的类型,IVisitor<int, Foo>和IVisitor<int, Bar>.请注意,规范给出的示例几乎与您的示例相同.
由于唯一性约束,因此不对此方法参数进行推断.由于编译器无法从参数中推断出任何类型信息TResult,因此无法继续尝试推断并因此失败.
至于为什么存在唯一性约束,我的猜测是它简化了算法并因此简化了编译器实现.如果您感兴趣,这里是源代码的链接,其中Roslyn(现代C#编译器)实现了泛型方法类型推断.
在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为工作场所实现一个通用接口.