c#中的通用接口类型推断怪异
Generic interface type inference weirdness in c#
在这种非常明显的情况下,C# 无法推断类型参数:
public void Test<T>(IEnumerable<KeyValuePair<string, T>> kvp)
{
Console.WriteLine(kvp.GetType().Name + ": KeyValues");
}
Test(new Newtonsoft.Json.Linq.JObject());
JObject
类型显然实现了 IEnumerable<KeyValuePair<string, JToken>>
,但我得到以下错误:
CS0411: The type arguments for method cannot be inferred from the usage.
为什么会这样?
UPD:致将此问题标记为重复的编辑:请注意我的方法签名不接受 IEnumerable<T>
,而是接受 IEnumerable<KeyValuePair<string, T>>
。 JObject
类型实现了 IEnumerable
两次,但只有一个实现符合此约束 - 因此应该没有歧义。
UPD:这是一个没有 JObject
的完整独立复制:
https://gist.github.com/impworks/2eee2cd0364815ab8245b81963934642
问题如下。
如果您不指定类型,那么它会尝试自动推断,但如果它变得混乱,则会抛出指定的异常。
在你的例子中,JObject 已经实现了接口 IEnumerable<KeyValuePair<string, JToken>>
。
JObject 还实现了 JContainer,它还实现了 IEnumerable< JToken>
。
所以当它被指定为 T 时,它会混淆 IEnumerable<JToken>
和 IEnumrable<KeyValuePair<string, JToken>>
。
我怀疑当设计 C# 的人在考虑编译器如何推断泛型类型参数的类型时,他们考虑了潜在的问题。
考虑以下场景:
class MyClass : JObject, IEnumerable<KeyValuePair<string, int>>
{
public IEnumerator<KeyValuePair<string, int>> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Foo : MyClass { }
class FooBar : Foo { }
public void Test<T>(IEnumerable<KeyValuePair<string, T>> kvp)
{
Console.WriteLine(kvp.GetType().Name + ": KeyValues");
}
var fooBar = new FooBar();
Test(fooBar);
应该将 T
推断为哪种类型? int
或 JToken
?
还有算法应该有多复杂?
大概这就是为什么编译器只在没有歧义的情况下才推断类型的原因。它是这样设计的,并非没有原因。
这是一个更简单的重现:
interface I<T> {}
class X<T> {}
class Y {}
class Z {}
class C : I<X<Y>>, I<Z> {}
public class P
{
static void M<T>(I<X<T>> i) { }
public static void Main()
{
M(new C());
}
}
类型推断失败。你问为什么,为什么问题总是难以回答,那我改一下问题:
What line of the specification disallows this inference?
我手边有一份 C# 3 规范;该行如下
V
是我们推断 到 的类型,所以 I<X<T>>
在这种情况下
U
是我们从 推断出的类型 ,所以在这种情况下 C
这在 C# 4 中会略有不同,因为我添加了协方差,但出于讨论的目的我们可以忽略它。
... if V
is a constructed type C<V1, … Vk>
and there is a unique set of types U1, … Uk
such that an implicit conversion exists from U
to C<U1, … Uk>
then an exact inference is made from each Ui
to the corresponding Vi
. Otherwise no inferences are made.
请注意其中的 unique 这个词。没有一组唯一的类型使得 C
可以转换为 I<Something>
,因为 X<Y>
和 Z
都是有效的。
这是另一个非为什么问题:
What factors were considered when the design team made this decision?
你是对的,理论上我们可以在你的情况下检测到 X<Y>
是有意的而 Z
不是。如果您想提出一种类型推断算法,它可以处理像这样的非唯一情况并且永远不会出错——请记住,Z
可以是 X<Y>
或 [=30 的子类型或超类型=] 和 I
可以是协变的——那么我相信 C# 团队会很乐意考虑你的提议。
我们在 2005 年设计 C# 3 类型推理算法时曾有过这样的争论,并认为一个 class 实现两个相同接口的情况很少见,而处理这些罕见情况会造成相当大的复杂性语言。设计、指定、实施和测试这些复杂问题的成本很高,而且我们还有其他事情可以花钱和精力去做,这些事情会产生更大的影响。
此外,我们在制作 C# 3 时并不知道我们是否会在 C# 4 中添加协变。我们从不想引入一种新的语言功能,这会使未来可能的语言功能变得不可能或困难。最好现在就在语言中设置限制,然后再考虑删除它们,而不是为一个罕见的场景做大量工作,这使得下一个版本中的常见场景变得困难。
事实上,我帮助设计了这个算法并实施了多次,一开始完全不记得这个规则,这应该告诉你在过去的 13 年里这个问题出现的频率。几乎没有。很少有人会在您的特定船上,并且有一个简单的解决方法:指定类型。
一个你没有问但想到的问题:
Could the error message be better?
是的。我为此道歉。我做了很多工作,使重载解析错误消息对常见的 LINQ 场景更具描述性,而且我一直想返回并使其他类型推断错误消息更加清晰。我编写了类型推断和重载解析代码来维护内部信息,解释为什么推断出一个类型或者选择或拒绝一个重载,这既是为了我自己的调试目的,也是为了做出更好的错误消息,但我从来没有抽出时间来公开那些信息给用户。总是有更高优先级的事情。
您可以考虑在 Roslyn GitHub 站点输入一个问题,建议改进此错误消息以帮助用户更轻松地诊断情况。同样,我没有立即诊断出问题而不得不返回到规范,这表明消息不清楚。
在这种非常明显的情况下,C# 无法推断类型参数:
public void Test<T>(IEnumerable<KeyValuePair<string, T>> kvp)
{
Console.WriteLine(kvp.GetType().Name + ": KeyValues");
}
Test(new Newtonsoft.Json.Linq.JObject());
JObject
类型显然实现了 IEnumerable<KeyValuePair<string, JToken>>
,但我得到以下错误:
CS0411: The type arguments for method cannot be inferred from the usage.
为什么会这样?
UPD:致将此问题标记为重复的编辑:请注意我的方法签名不接受 IEnumerable<T>
,而是接受 IEnumerable<KeyValuePair<string, T>>
。 JObject
类型实现了 IEnumerable
两次,但只有一个实现符合此约束 - 因此应该没有歧义。
UPD:这是一个没有 JObject
的完整独立复制:
https://gist.github.com/impworks/2eee2cd0364815ab8245b81963934642
问题如下。
如果您不指定类型,那么它会尝试自动推断,但如果它变得混乱,则会抛出指定的异常。
在你的例子中,JObject 已经实现了接口
IEnumerable<KeyValuePair<string, JToken>>
。JObject 还实现了 JContainer,它还实现了
IEnumerable< JToken>
。
所以当它被指定为 T 时,它会混淆 IEnumerable<JToken>
和 IEnumrable<KeyValuePair<string, JToken>>
。
我怀疑当设计 C# 的人在考虑编译器如何推断泛型类型参数的类型时,他们考虑了潜在的问题。
考虑以下场景:
class MyClass : JObject, IEnumerable<KeyValuePair<string, int>>
{
public IEnumerator<KeyValuePair<string, int>> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Foo : MyClass { }
class FooBar : Foo { }
public void Test<T>(IEnumerable<KeyValuePair<string, T>> kvp)
{
Console.WriteLine(kvp.GetType().Name + ": KeyValues");
}
var fooBar = new FooBar();
Test(fooBar);
应该将 T
推断为哪种类型? int
或 JToken
?
还有算法应该有多复杂?
大概这就是为什么编译器只在没有歧义的情况下才推断类型的原因。它是这样设计的,并非没有原因。
这是一个更简单的重现:
interface I<T> {}
class X<T> {}
class Y {}
class Z {}
class C : I<X<Y>>, I<Z> {}
public class P
{
static void M<T>(I<X<T>> i) { }
public static void Main()
{
M(new C());
}
}
类型推断失败。你问为什么,为什么问题总是难以回答,那我改一下问题:
What line of the specification disallows this inference?
我手边有一份 C# 3 规范;该行如下
V
是我们推断 到 的类型,所以I<X<T>>
在这种情况下U
是我们从 推断出的类型 ,所以在这种情况下C
这在 C# 4 中会略有不同,因为我添加了协方差,但出于讨论的目的我们可以忽略它。
... if
V
is a constructed typeC<V1, … Vk>
and there is a unique set of typesU1, … Uk
such that an implicit conversion exists fromU
toC<U1, … Uk>
then an exact inference is made from eachUi
to the correspondingVi
. Otherwise no inferences are made.
请注意其中的 unique 这个词。没有一组唯一的类型使得 C
可以转换为 I<Something>
,因为 X<Y>
和 Z
都是有效的。
这是另一个非为什么问题:
What factors were considered when the design team made this decision?
你是对的,理论上我们可以在你的情况下检测到 X<Y>
是有意的而 Z
不是。如果您想提出一种类型推断算法,它可以处理像这样的非唯一情况并且永远不会出错——请记住,Z
可以是 X<Y>
或 [=30 的子类型或超类型=] 和 I
可以是协变的——那么我相信 C# 团队会很乐意考虑你的提议。
我们在 2005 年设计 C# 3 类型推理算法时曾有过这样的争论,并认为一个 class 实现两个相同接口的情况很少见,而处理这些罕见情况会造成相当大的复杂性语言。设计、指定、实施和测试这些复杂问题的成本很高,而且我们还有其他事情可以花钱和精力去做,这些事情会产生更大的影响。
此外,我们在制作 C# 3 时并不知道我们是否会在 C# 4 中添加协变。我们从不想引入一种新的语言功能,这会使未来可能的语言功能变得不可能或困难。最好现在就在语言中设置限制,然后再考虑删除它们,而不是为一个罕见的场景做大量工作,这使得下一个版本中的常见场景变得困难。
事实上,我帮助设计了这个算法并实施了多次,一开始完全不记得这个规则,这应该告诉你在过去的 13 年里这个问题出现的频率。几乎没有。很少有人会在您的特定船上,并且有一个简单的解决方法:指定类型。
一个你没有问但想到的问题:
Could the error message be better?
是的。我为此道歉。我做了很多工作,使重载解析错误消息对常见的 LINQ 场景更具描述性,而且我一直想返回并使其他类型推断错误消息更加清晰。我编写了类型推断和重载解析代码来维护内部信息,解释为什么推断出一个类型或者选择或拒绝一个重载,这既是为了我自己的调试目的,也是为了做出更好的错误消息,但我从来没有抽出时间来公开那些信息给用户。总是有更高优先级的事情。
您可以考虑在 Roslyn GitHub 站点输入一个问题,建议改进此错误消息以帮助用户更轻松地诊断情况。同样,我没有立即诊断出问题而不得不返回到规范,这表明消息不清楚。