C# 泛型方法类型参数不是从使用中推断出来的 [英] C# generic method type argument not inferred from usage

查看:34
本文介绍了C# 泛型方法类型参数不是从使用中推断出来的的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

最近我尝试了访问者模式的实现,我试图在其中强制执行 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)' 的类型参数."

谁能解释一下这是为什么?IVisitor 接口只有一种版本是 CountVisitor 实现的 - 或者,如果 IVisitor 可以出于某种原因不会被淘汰,它们都具有相同的 T - int,= 无论如何没有其他类型可以在那里工作.一旦有不止一个合适的候选者,类型推断就会放弃吗?(有趣的事实:ReSharper 认为 theFoo.Accept(...) 中的 int 是多余的 :P,即使没有它也无法编译)

解决方案

<块引用>

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

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

<小时>

来自 C# 5 规范(最最近我能找到),§7.5.2:

<块引用>

Tr M(T1 x1 … Tm xm)

使用 M(E1 …Em) 形式的方法调用,类型推断的任务是找到唯一的类型参数S1…Sn 用于每个类型参数 X1…Xn 使得调用 M(E1…Em) 变为有效.

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

<块引用>

对于每个方法参数Ei:

  • 如果 Ei 是匿名函数,则 显式参数类型推断(第 7.5.2.7 节)由 EiTi

  • 否则,如果 Ei 有一个类型 U 并且 xi 是一个值参数,那么一个下限推断 U Ti.

你只有一个参数,所以我们有唯一的 Ei 是表达式 new CountVisitor().它显然不是匿名函数,所以我们在第二个要点中.很容易看出,在我们的例子中,UCountVisitor 类型.xi 是一个值参数"位基本上意味着它不是 outinref 等变量,这里就是这种情况.

此时,我们现在需要对 CountVisitorIVisitor 的相关部分进行下界推断§7.5.2.9(其中由于变量开关,我们有 V = IVisitor 在我们的例子中):

<块引用>
  • 否则,集合 U1…UkV1…Vk 将通过检查以下任何一种情况是否适用来确定:
    • V 是数组类型 V1[…]U 是数组类型 U1[…](或有效基类型为 U1[…] 的类型参数)相同等级
    • VIEnumerableICollectionIListU为一维数组类型U1[](或有效基类型为U1[]的类型参数)
    • V 是构造的类、结构、接口或委托类型 C 并且有一个唯一类型 C 使得 U(或者,如果 U 是一个类型参数,它的有效基类或其有效接口集的任何成员)是相同的,继承自(直接或间接),或实现(直接或间接)C.

(唯一性"限制意味着在接口C{} class U: C, C{} 的情况下,那么在推断时不进行推断从 UC 因为 U1 可以是 XY.)

我们可以跳过前两种情况,因为它们显然不适用,第三种情况就是我们所遇到的情况.编译器尝试找到 CountVisitor 实现的 唯一 类型 C 并找到 两种 这样的类型、IVisitorIVisitor.请注意,规范给出的示例与您的示例几乎相同.

由于唯一性约束,没有对该方法参数进行推断.由于编译器无法从参数中推断出任何类型信息,因此无法继续尝试推断 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 arguments S1…Sn for each of the type parameters X1…Xn so that the call M<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 from Ei to Ti

  • Otherwise, if Ei has a type U and xi is a value parameter then a lower-bound inference is made from U to Ti.

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 and V1…Vk are determined by checking if any of the following cases apply:
    • V is an array type V1[…] and U is an array type U1[…] (or a type parameter whose effective base type is U1[…]) of the same rank
    • V is one of IEnumerable<V1>, ICollection<V1> or IList<V1> and U is a one-dimensional array type U1[] (or a type parameter whose effective base type is U1[])
    • V is a constructed class, struct, interface or delegate type C<V1…Vk> and there is a unique type C<U1…Uk> such that U (or, if U 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 from U to C<T> because U1 could be X or Y.)

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆