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

问题如下。

如果您不指定类型,那么它会尝试自动推断,但如果它变得混乱,则会抛出指定的异常。

  1. 在你的例子中,JObject 已经实现了接口 IEnumerable<KeyValuePair<string, JToken>>

  2. 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 推断为哪种类型? intJToken ?

还有算法应该有多复杂?

大概这就是为什么编译器只在没有歧义的情况下才推断类型的原因。它是这样设计的,并非没有原因。

这是一个更简单的重现:

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 站点输入一个问题,建议改进此错误消息以帮助用户更轻松地诊断情况。同样,我没有立即诊断出问题而不得不返回到规范,这表明消息不清楚。