C# 泛型方法类型参数不是从使用中推断出来的 [英] C# generic method type argument not inferred from usage
问题描述
最近我尝试了访问者模式的实现,我试图在其中强制执行 Accept &访问具有通用接口的方法:
公共接口IVisitable其中 TVisitable : IVisitable{TResult Accept(IVisitorvisitor);}
- 其目的是 1) 将某种类型的Foo"标记为此类访问者可访问的,而后者又是此类 Foo 类型的访问者"和 2) 在实现的可访问类型上强制执行正确签名的 Accept 方法,像这样:
public class Foo : IVisitable{public TResult Accept(IVisitorvisitor) =>访客.访问(这个);}
到目前为止一切顺利,访问者界面:
public interface IVisitor其中 TVisitable : IVisitable{TResult 访问(TVisitable 可访问);}
- 应该 1) 将访问者标记为能够访问"TVisitable 2) 此 TVisitable 的结果类型 (TResult) 应该是什么 3) 强制每个 TVisitable 的访问方法正确签名,访问者实现是能够访问"访问",像这样:
公共类CountVisitor:IVisitor{公共 int 访问(Foo 可访问)=>42;}公共类 NameVisitor : IVisitor{公共字符串访问(Foo 可访问)=>咀嚼";}
非常愉快&漂亮,这让我写:
var theFoo = new Foo();int count = theFoo.Accept(new CountVisitor());字符串名称 = theFoo.Accept(new NameVisitor());
非常好.
现在悲伤的时刻开始了,当我添加另一种可访问类型时,例如:
public class Bar : IVisitable{public TResult Accept(IVisitorvisitor) =>访客.访问(这个);}
可以通过 CountVisitor
访问:
public class CountVisitor : IVisitor, IVisitor{公共 int 访问(Foo 可访问)=>42;公共 int 访问(酒吧可访问)=>7;}
这突然打破了 Accept 方法中的类型推断!(这破坏了整个设计)
var theFoo = new Foo();int count = theFoo.Accept(new CountVisitor());
给我:
<块引用>无法从用法推断方法 'Foo.Accept
的类型参数."
谁能解释一下这是为什么?IVisitor
接口只有一种版本是 CountVisitor
实现的 - 或者,如果 IVisitor
可以出于某种原因不会被淘汰,它们都具有相同的 T
- int
,= 无论如何没有其他类型可以在那里工作.一旦有不止一个合适的候选者,类型推断就会放弃吗?(有趣的事实:ReSharper 认为 theFoo.Accept
中的 int
是多余的 :P,即使没有它也无法编译)
<块引用>
一旦有不止一个合适的候选者,类型推断就会放弃吗?
是的,在这种情况下确实如此.在尝试推断方法的泛型类型参数 (TResult
) 时,类型推断算法似乎在 CountVisitor
对类型 IVisitor
.
来自 C# 5 规范(最最近我能找到),§7.5.2:
<块引用>Tr M
使用 M(E1 …Em)
形式的方法调用,类型推断的任务是找到唯一的类型参数S1…Sn
用于每个类型参数 X1…Xn
使得调用 M
变为有效.
编译器采取的第一步如下(§7.5.2.1):
<块引用>对于每个方法参数Ei
:
如果
Ei
是匿名函数,则 显式参数类型推断(第 7.5.2.7 节)由Ei
Ti
否则,如果
Ei
有一个类型U
并且xi
是一个值参数,那么一个下限推断 从U
到Ti
.
你只有一个参数,所以我们有唯一的 Ei
是表达式 new CountVisitor()
.它显然不是匿名函数,所以我们在第二个要点中.很容易看出,在我们的例子中,U
是 CountVisitor
类型.xi
是一个值参数"位基本上意味着它不是 out
、in
、ref
等变量,这里就是这种情况.
此时,我们现在需要对 CountVisitor
到 IVisitor
的相关部分进行下界推断§7.5.2.9(其中由于变量开关,我们有 V
= IVisitor
在我们的例子中):
- 否则,集合
U1…Uk
和V1…Vk
将通过检查以下任何一种情况是否适用来确定:V
是数组类型V1[…]
和U
是数组类型U1[…]
(或有效基类型为U1[…]
的类型参数)相同等级V
是IEnumerable
、ICollection
或IList
和U
为一维数组类型U1[]
(或有效基类型为U1[]
的类型参数)V
是构造的类、结构、接口或委托类型C
并且有一个唯一类型C
使得U
(或者,如果U
是一个类型参数,它的有效基类或其有效接口集的任何成员)是相同的,继承自(直接或间接),或实现(直接或间接)C
.
(唯一性"限制意味着在接口C
的情况下,那么在推断时不进行推断从 U
到 C
因为 U1
可以是 X
或 Y
.)
我们可以跳过前两种情况,因为它们显然不适用,第三种情况就是我们所遇到的情况.编译器尝试找到 CountVisitor
实现的 唯一 类型 C
并找到 两种 这样的类型、IVisitor
和 IVisitor
.请注意,规范给出的示例与您的示例几乎相同.
由于唯一性约束,没有对该方法参数进行推断.由于编译器无法从参数中推断出任何类型信息,因此无法继续尝试推断 TResult
并因此失败.
至于为什么存在唯一性约束,我的猜测是它简化了算法,从而简化了编译器的实现.如果您有兴趣,这是一个链接,指向 Roslyn(现代 C# 编译器)实现泛型方法类型推断的源代码.
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)
Does the type inference give up as soon as there are more than just one suitable candidate?
Yes, in this case it does. While attempting to infer the method's generic type parameter (TResult
), the type inference algorithm appears to fail on CountVisitor
having two inferences to the type IVisitor<TResult, TVisitable>
.
From the C# 5 specification (the most recent I could find), §7.5.2:
Tr M<X1…Xn>(T1 x1 … Tm xm)
With a method call of the form
M(E1 …Em)
the task of type inference is to find unique type argumentsS1…Sn
for each of the type parametersX1…Xn
so that the callM<S1…Sn>(E1…Em)
becomes valid.
The very first step the compiler takes is as follows (§7.5.2.1):
For each of the method arguments
Ei
:
If
Ei
is an anonymous function, an explicit parameter type inference (§7.5.2.7) is made fromEi
toTi
Otherwise, if
Ei
has a typeU
andxi
is a value parameter then a lower-bound inference is made fromU
toTi
.
You only have one argument, so we have that the only Ei
is the expression new CountVisitor()
. It's clearly not an anonymous function, so we're in the second bullet point. It's trivial to see that in our case, U
is of type CountVisitor
. The "xi
is a value parameter" bit basically means it's not an out
, in
, ref
etc. variable, which is the case here.
At this point, we now need to make a lower-bound inference from CountVisitor
to IVisitor<TResult, TVisitable>
The relevant part of §7.5.2.9 (where due to a variable switch, we have V
= IVisitor<TResult, TVisitable>
in our case):
- Otherwise, sets
U1…Uk
andV1…Vk
are determined by checking if any of the following cases apply:
V
is an array typeV1[…]
andU
is an array typeU1[…]
(or a type parameter whose effective base type isU1[…]
) of the same rankV
is one ofIEnumerable<V1>
,ICollection<V1>
orIList<V1>
andU
is a one-dimensional array typeU1[]
(or a type parameter whose effective base type isU1[]
)V
is a constructed class, struct, interface or delegate typeC<V1…Vk>
and there is a unique typeC<U1…Uk>
such thatU
(or, ifU
is a type parameter, its effective base class or any member of its effective interface set) is identical to, inherits from (directly or indirectly), or implements (directly or indirectly)C<U1…Uk>
.(The "uniqueness" restriction means that in the case interface
C<T>{} class U: C<X>, C<Y>{}
, then no inference is made when inferring fromU
toC<T>
becauseU1
could beX
orY
.)
We can skip past the first two cases as they're clearly not applicable, the third case is the one we fall into. The compiler attempts to find a unique type C<U1…Uk>
that CountVisitor
implements and finds two such types, IVisitor<int, Foo>
and IVisitor<int, Bar>
. Note that the example the spec gives is nearly identical your example.
Because of the uniqueness constraint, no inference is made for this method argument. With the compiler not able to infer any type information from the argument, it has nothing to go on to try to infer TResult
and thus fails.
As to why there exists a uniqueness constraint, my guess is that it simplifies the algorithm and thus compiler implementation. If you're interested, here's a link to source code where Roslyn (modern C# compiler) implements generic method type inference.
这篇关于C# 泛型方法类型参数不是从使用中推断出来的的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!