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;
}
}
通过这样做,您将不需要在 Foo
和 Bar
上实现 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()
。它显然不是匿名函数,所以我们在第二个要点。在我们的例子中很容易看出,U
是 CountVisitor
类型。 “xi
是一个值参数”位基本上意味着它不是 out
、in
、ref
等变量,这里就是这种情况。
此时,我们现在需要从CountVisitor
到IVisitor<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# 编译器)实现泛型方法类型推断的源代码。
最近我尝试了访问者模式的实现,我尝试使用通用接口强制执行 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;
}
}
通过这样做,您将不需要在 Foo
和 Bar
上实现 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 argumentsS1…Sn
for each of the type parametersX1…Xn
so that the callM<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 fromEi
toTi
Otherwise, if
Ei
has a typeU
andxi
is a value parameter then a lower-bound inference is made fromU
toTi
.
你只有一个参数,所以我们只有 Ei
是表达式 new CountVisitor()
。它显然不是匿名函数,所以我们在第二个要点。在我们的例子中很容易看出,U
是 CountVisitor
类型。 “xi
是一个值参数”位基本上意味着它不是 out
、in
、ref
等变量,这里就是这种情况。
此时,我们现在需要从CountVisitor
到IVisitor<TResult, TVisitable>
做一个下限推断 §7.5.2.9的相关部分(由于可变开关,在我们的例子中我们有 V
= IVisitor<TResult, TVisitable>
):
- 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
.)
我们可以跳过前两种情况,因为它们显然不适用,第三种情况就是我们遇到的情况。编译器试图找到 CountVisitor
实现的 unique 类型 C<U1…Uk>
并找到 two 这样的类型,IVisitor<int, Foo>
和 IVisitor<int, Bar>
。请注意,规范给出的示例与您的示例几乎相同。
由于唯一性约束,没有对该方法参数进行推断。由于编译器无法从参数中推断出任何类型信息,因此无法继续尝试推断 TResult
,因此失败。
至于为什么存在唯一性约束,我的猜测是它简化了算法,从而简化了编译器实现。如果您有兴趣,here's a link 到 Roslyn(现代 C# 编译器)实现泛型方法类型推断的源代码。