为什么类型约束不是方法签名的一部分?

12

更新: 截至C# 7.3版本,这个问题不再存在。从发行说明中可以看到:

当一个方法组包含一些泛型方法,其类型参数不满足其约束条件时,这些成员将被从候选集中移除。

C# 7.3之前:

我阅读了Eric Lippert的“约束不是签名的一部分”,现在我明白规范指定类型约束在重载解析之后进行检查,但我仍然不清楚为什么必须这样做。以下是Eric的例子:

static void Foo<T>(T t) where T : Reptile { }
static void Foo(Animal animal) { }
static void Main() 
{ 
    Foo(new Giraffe()); 
}

这个代码无法编译,因为对于Foo(new Giraffe())的重载解析推断出Foo<Giraffe>是最佳匹配,但是类型约束失败并抛出编译时错误。用Eric的话来说:

原则上,重载解析(和方法类型推断)在参数列表和每个候选方法的形式参数列表之间寻找最佳匹配。也就是说,它们查看候选方法的签名。

类型约束不是签名的一部分,但为什么不能成为签名的一部分?有哪些场景不考虑类型约束会是一个坏主意?它只是难以实现还是不可能实现?我不是在主张如果选择的最佳重载由于某些原因无法调用,则悄悄地回退到第二个最佳重载;我会讨厌那样做。我只是想理解为什么类型约束不能用于影响选择最佳重载。
我想象在C#编译器内部,仅用于重载解析目的(它不会永久重写方法),以下内容:
static void Foo<T>(T t) where T : Reptile { }

被转换为:

static void Foo(Reptile  t) { }

为什么不能将类型约束“拉入”正式参数列表中?这会以何种不良方式改变签名呢?我觉得这只会增强签名。然后Foo<Reptile>就永远不会被视为重载候选项。 编辑2:难怪我的问题如此混乱。我没有正确阅读Eric的博客,并引用了错误的示例。我编辑了我认为更合适的示例。我还更改了标题以使其更具体。这个问题似乎不像我最初想象的那样简单,也许我错过了一些重要的概念。我不太确定这是否适合stackoverflow材料,最好将这个问题/讨论移动到其他地方。

1
你在问题开头引用的有关鬣蜥的那一段话是为了说明一种情况,即类型推断确实会考虑到C<T>中T的约束条件,因此在这个例子中重载解析最终选择了非泛型方法。你确定这是你想引用来提出这个问题的相关文章部分吗?问题的其余部分似乎与它不符合逻辑。 - Eric Lippert
1
我认为你想问的是,当类型推断成功但推断出的类型违反了方法类型参数的约束时,为什么在存在替代方案时会导致重载决议失败。我发现这个问题非常令人困惑,但是再怎么说,这确实是规范中一个令人困惑的部分。 - Eric Lippert
你是对的。我读错了你的博客并使用了错误的例子。难怪我的问题如此混淆。我已经尽力澄清我的问题;我还将阅读您其他的博客文章,看看是否可以提高我的理解能力。 - Daryl
我注意到Eric的博客文章有9页评论(http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx?PageIndex=1#comments),其中Eric写了许多回复,回答了许多像我这样的问题。如果我有时间,我打算尝试编制Eric相关回复的汇编。 - Daryl
2个回答

6

C#编译器不能将类型约束视为方法签名的一部分,因为它们不是CLR方法签名的一部分。如果重载决策在不同语言中工作方式不同(主要是由于运行时可能发生的动态绑定,并且不应该与其他语言不同),那将是灾难性的,否则所有地狱都会爆发。

为什么决定这些约束不是CLR方法签名的一部分是另一个问题,我只能对此做出错误的推测。我会让知道的人来回答。


1
但这只是把问题从“为什么它不是C#的一部分”转移到了“为什么它不是CLR的一部分”的问题上...支持考虑类型约束的重载解析存在一个主要问题(请参见我的答案)。 - Eric J.
有几个 .Net 语言中有一些其他语言没有实现的功能。主要原因是辅助语法可以轻松转换为正确的语法。这只是其中非常简单的一个例子,仅涉及确定要放置哪个方法调用。此外,在语言中可能存在其他可能的歧义情况,并且编译器完全可以使用它找到的内容或在无法解析特定方法调用时给出错误,就像在其他几十种情况下一样。语言特性不是 VM 特性。 - Tamir Daniely

1
如果 T 匹配多个约束条件,则会创建一个无法自动解决的歧义。例如,您有一个带有约束条件的通用类: where T : IFirst 另一个带有约束条件的类: where T : ISecond 现在,您希望 T 是实现了 IFirstISecond 的类。
具体的代码示例:
public interface IFirst
{
    void F();
}

public interface ISecond
{
    void S();
}

// Should the compiler pick this "overload"?
public class My<T> where T : IFirst
{
}

// Or this one?
public class My<T> where T : ISecond
{
}

public class Foo : IFirst, ISecond
{
    public void Bar()
    {
        My<Foo> myFoo = new My<Foo>();
    }

    public void F() { }
    public void S() { }
}

这会导致名称冲突; 在我的问题中,错误是由编译器不知道为方法调用选择哪个方法重载引起的: Bar(new Iguana(), null)。我可能有点慢,但我还没有看到这两者之间的联系。我将继续研究你的例子。 - Daryl
4
@EricJ,即使不使用泛型,您也可以导致方法的歧义性——如果一个方法的两个重载分别接受IFirstISecond类型的参数,并且您尝试使用Foo类型的参数解析要调用哪个方法时,将发生相同的情况。我不认为这就是C#团队决定不将其作为方法签名一部分的原因。 - Chris Shain
@ChrisShain 嗯,但是如果你的函数调用对于 IFirst 和 ISecond 存在歧义,编译器会报错并且你可以通过将参数转换为所需的接口来解决问题。如果泛型约束是方法签名的一部分,你需要在调用处使用一种语法来选择不同的约束,而任何对库的泛型约束的更改都意味着调用代码必须重新编译才能继续工作,我认为这不是最理想的情况。 - Mike Marynowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接