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

Posted

技术标签:

【中文标题】C# 泛型方法类型参数不是从用法中推断出来的【英文标题】:C# generic method type argument not inferred from usage 【发布时间】:2019-01-24 19:32:40 【问题描述】:

最近我尝试了访问者模式的实现,我尝试使用通用接口强制执行 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) 将访问者标记为“能够访问”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";

非常愉快和美丽,这让我可以写:

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&lt;TResult&gt;(IVisitor&lt;TResult, Foo&gt;)' 的类型参数。”

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

【问题讨论】:

【参考方案1】:

似乎类型推断以一种贪婪的方式工作,首先尝试匹配 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());
    

其次(这是突出类型推断如何工作的奇怪部分)看看如果在Bar 访问者中将int 替换为string 会发生什么:

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&lt;string, Foo&gt;'

这表明编译器首先查看 method 泛型类型(在您的情况下为TResult),如果找到更多候选者则立即失败。它甚至没有进一步研究类泛型类型。

我尝试从 Microsoft 查找类型推断规范,但找不到。

【讨论】:

非常有趣!感谢您的回答:)我想我们已经接近了!虽然,关于 First: yes,它可以工作,因为你已经手动指定了所有类型参数,所以不需要推断任何东西,只是类型检查必须通过。第二:您已经强制 string Accept&lt;string&gt;(IVisitor&lt;string, Foo&gt;) 并且是的,CountVisitor 不能转换为该 arg,就像 bool 不能转换为 double - 不确定这是否真的证明了什么。不过,我会考虑“贪婪推理”,这听起来很有希望。再次感谢您的回答! 我考虑过“贪婪推理”——最终得出了不同的理论:类型检查器和推理器不是朋友——他们不互相交谈,不分享“知识”。 1. 类型检查器检查类型是可转换的 - 如果是,那么 2. 新一轮,推断器试图找出类型 - 哪种类型? - IVisitor 中的 TResult(不知何故忽略了 TVisitable 必须是 Foo 的事实) - 找到两个这样的接口 - 放弃。不知道如何证明这一点,它只是“表现”那样(可能是因为您提到的“方法参数优先”推断?)【参考方案2】:

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

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


来自C# 5 specification(我能找到的最新版本),§7.5.2:

Tr M&lt;X1…Xn&gt;(T1 x1 … Tm xm)

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

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

对于每个方法参数Ei

如果Ei 是匿名函数,则显式参数类型推断(第7.5.2.7 节)由Ei 生成 给Ti

否则,如果Ei 的类型为U,而xi 是值参数,则下界推断 来自 @ 987654338@ Ti

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

此时,我们现在需要从CountVisitorIVisitor&lt;TResult, TVisitable&gt; 进行下界推断 §7.5.2.9 的相关部分(由于变量切换,我们有V = IVisitor&lt;TResult, TVisitable&gt; 在我们的例子中):

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

(“唯一性”限制意味着在案例接口C&lt;T&gt; class U: C&lt;X&gt;, C&lt;Y&gt; 中,从U 推断到C&lt;T&gt; 时不会进行推断,因为U1 可能是XY。)

我们可以跳过前两种情况,因为它们显然不适用,第三种情况是我们遇到的情况。编译器尝试找到CountVisitor 实现的唯一 类型C&lt;U1…Uk&gt;,并找到两个 这样的类型,IVisitor&lt;int, Foo&gt;IVisitor&lt;int, Bar&gt;。请注意,规范给出的示例与您的示例几乎相同。

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


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

【讨论】:

感谢您的回答!这个很有道理,看看代码吧【参考方案3】:

在 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 上实现 Accept 方法,尽管它们仍然必须实现 Visitor 的通用接口才能正常工作。

【讨论】:

哇,这是个有用的技巧:)谢谢你的信息!虽然,我的目标是类型安全,并且对为什么在这种特殊情况下类型推断失败很感兴趣——因为它让我感到惊讶,因为在我看来只有一个合适的类型

以上是关于C# 泛型方法类型参数不是从用法中推断出来的的主要内容,如果未能解决你的问题,请参考以下文章

请教关于java的泛型方法

Swift 中的泛型 - “无法推断出泛型参数‘T’

Typescript 推断的泛型类型在条件类型中是未知的

C#中的泛型是啥意思?

为啥 C# 无法从非泛型静态方法的签名推断泛型类型参数类型?

详解C#泛型