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

C# generic method type argument not inferred from usage

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

public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable>
{
    TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor);
}

- 其目的是 1) 将特定类型 "Foo" 标记为可由此类访问者访问,而后者又是 "visitor of such type 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) 将访问者标记为 "able to visit" TVisitable 2) 此 TVisitable 的结果类型 (TResult) 应该是什么 3) 为访问者实施的每个 TVisitable 强制执行正确签名的 Visit 方法"able to visit",像这样:

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());

给我:

"The type arguments for method 'Foo.Accept<TResult>(IVisitor<TResult, Foo>)' cannot be inferred from the usage."

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

在 C# 中,您可以通过使用 dynamic 关键字删除 'double dispatch' 来简化访问者模式。

您可以像这样实现您的访客:

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 的通用接口才能正常工作.

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

int count = theFoo.Accept<int>(new CountVisitor());

它有效,这很奇怪,因为 Foo 是 class 泛型类型的唯一候选者。

首先,如果将方法泛型替换为第二个 class 泛型,它会起作用:

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());

error CS1503: Argument 1: cannot convert from 'CountVisitor' to 'IVisitor<string, Foo>'

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

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

Does the type inference give up as soon as there are more than just one suitable candidate?

是的,在这种情况下是这样。在尝试推断方法的泛型类型参数 (TResult) 时,类型推断算法似乎在 CountVisitor 上失败,对类型 IVisitor<TResult, TVisitable>.

有两个推断

来自 C# 5 specification(我能找到的最新的),§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.

编译器执行的第一步如下 (§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.

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

此时,我们现在需要从CountVisitorIVisitor<TResult, TVisitable>做一个下限推断 §7.5.2.9的相关部分(由于可变开关,在我们的例子中我们有 V = IVisitor<TResult, TVisitable>):

  • 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.)

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

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


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