通用方法和方法重载

21

方法重载允许我们定义多个具有相同名称但不同参数集(因此具有相同名称但不同签名)的方法。

这两个方法是否已经重载?

class A
{
    public static void MyMethod<T>(T myVal) { }
    public static void MyMethod(int myVal) { }
}

编辑:

语句 A<int>.MyMethod(myInt); 应该会抛出错误,因为构造类型 A<int> 有两个名称和签名相同的方法吗?


2
运行时总是会选择更具体的方法而不是通用方法。 - John Alexiou
6个回答

47
这两种方法是重载的吗?
是的。
声明类型"A.MyMethod(myInt);"不应该抛出错误,因为构造类型"A"具有相同签名的两个方法吗?
这个问题没有意义;正如您所声明的那样,A不是一个泛型类型。也许你的意思是:
如果有两个模糊的候选方法,声明"A.MyMethod(myInt);"是否应该导致编译器报告错误?
不。在这种情况下,重载决议优先选择非泛型版本。有关更多详细信息,请参见下文。
或者你的意思是:
首先,类型A的声明是否非法,因为从某种意义上说它具有相同签名的两个方法"MyMethod"和"MyMethod"?
不。类型A是完全合法的。泛型度量是签名的一部分。因此,由于第一个方法具有泛型度量为零,第二个方法具有泛型度量为1,因此不存在具有相同签名的两个方法。
或者你的意思是:
class G<T> 
{
    public static void M(T t) {}
    public static void M(int t) {}
}

通用类型 G<T> 可以被构造,使其具有两个具有相同签名的方法。声明这样的类型是否合法?

是的,声明这样的类型是合法的。通常不建议这样做,但是它是合法的。

您可能会反驳:

但是我从Addison-Wesley出版的C# 2.0规范的第479页上看到“声明了两个名称相同的函数成员...必须具有参数类型,以便没有闭合的构造类型可以具有相同名称和签名的两个成员。”怎么回事?

当最初设计C# 2.0时,确实是这样计划的。然而,随后设计师意识到这种理想的模式将变得非法:

class C<T> 
{
    public C(T t) { ... } // Create a C<T> from a given T
    public C(Stream s) { ... } // Deserialize a C<T> from disk
}

抱歉,伙计,现在我们要说对不起了,因为你可能会写出 C<Stream>,导致两个构造函数合并,整个类都是非法的。那将是不幸的。显然,很少有人会使用 Stream 作为类型参数来构造这个东西!
不幸的是,在文本更新为最终版本之前,规范已经发布了。第479页上的规则不是我们实现的规则。
继续代表您提出一些问题:
所以,如果您调用 G<int>.M(123),或者在原始示例中调用 A.MyMethod(123),会发生什么?
当重载决议面临由于泛型构造而具有相同签名的两个方法时,认为泛型构造的方法比“自然”方法“不太具体”。较不具体的方法输给了更具体的方法。
那么,如果重载决议起作用,为什么这是一个坏主意呢? A.MyMethod 的情况并不太糟糕;通常很容易明确地确定哪种方法是预期的。但是 G<int>.M(123) 的情况要糟糕得多。CLR 规则使这种情况成为“实现定义行为”,因此任何事情都可能发生。从技术上讲,CLR 可以拒绝验证构造类型 G<int> 的程序。或者它可能会崩溃。实际上它都没有做;它尽力应对糟糕的情况。
这种类型构造是否有任何导致真正的实现定义行为的示例?

可以的。请查看以下文章以获得更多细节:

https://ericlippert.com/2006/04/05/odious-ambiguous-overloads-part-one/

https://ericlippert.com/2006/04/06/odious-ambiguous-overloads-part-two/


2
@aspnetonc: 正确。方法可以拥有相同的名称,但每个方法必须有独特的签名。C#语言规范将方法的签名定义为其名称、泛型度和参数类型的组合(并且指出仅有一个为“out”,另一个为“ref”的签名被认为是相同的签名)。CLR对“签名”的定义略有不同,因为它还将返回类型和参数类型以及返回类型上的modopt/modreq修饰符视为签名的一部分。 - Eric Lippert
2
@Joan:现在,lambda确实存在这个问题;如何绑定lambda内部的类型取决于其上下文,而上下文又可能取决于lambda内部。让它在常见情况下高效工作花费了我大部分时间,仍然存在一些病态情况,需要进行指数级别的绑定。这种情况仅限于嵌套很深的lambda,这种情况很少见。嵌套函数调用是常见的。总的来说,我们总是希望从表达式的内部到外部进行推理,而不是尝试双向推理。 - Eric Lippert
1
@Joan:这是可行的;我们知道这一点,因为我们使用lambda函数来实现它。但这意味着,如果您有嵌套n层的函数调用,每个函数都有m个重载,那么重载解析必须考虑m的n次方种可能性。我们必须考虑所有这些可能性,因为如果没有一个可行的,那么我们必须尝试所有可能性,如果有一个可行的,那么我们需要知道是否有第二个(以便我们可以给出模糊错误),而第二个可能是最后一个。这使得简单的程序编译时间任意延长,所以我们不会这样做。 - Eric Lippert
2
@User437291:我不明白你的问题。听起来你是在问「我是否应该不做一些不可能的事情?」——无论是不是道德问题,能否做到一些不可能的事情都是没有意义的;无论如何,你都不会去做一些不可能的事情。假设答案是「当然可以,继续吧,写一个不符合CLR方法签名唯一性规则的方法重载」。你要如何开始呢?难道你打算编写自己的元数据发射器,还是有其他什么计划? - Eric Lippert
2
因为CLR旨在实现除C#之外的其他语言。许多语言具有比C#规则更不严格的“签名冲突”规则。例如,在C++中,您可以拥有两种方法void M(const int& x)和void M(int& x),尽管它们的签名看起来非常相似,但它们是不同的方法。如果CLR不允许C++-to-IL编译器仅在const-ness上有所不同地进行两个重载,那么这将给编译器编写者带来负担,不是吗? - Eric Lippert
显示剩余10条评论

4
是的。当参数类型为 int 时,MyMethod(int myVal) 将被调用,对于所有其他参数,泛型重载将被调用,即使参数可以隐式转换为(或是派生类)硬编码类型。重载解析将选择最佳匹配,并且泛型重载将在编译时解析为精确匹配。
注意:正如 Steven Sudit 在他的答案中指出的那样,您可以显式调用泛型重载并使用 int,通过在方法调用中提供类型参数。
short s = 1;
int i = s;
MyMethod(s); // Generic
MyMethod(i); // int
MyMethod((int)s); // int
MyMethod(1); // int
MyMethod<int>(1); // Generic**
MyMethod(1.0); // Generic
// etc.

我认为第二部分并不完全正确:请查看我的答案。 - Steven Sudit
@Steven,显式调用通用重载是完全不同的问题。我会做个记录。 - Anthony Pegram
@aspnetonc,关于你的MyMethod(int i)和MyMethod<int>(T t)的编译方式不明确的问题,你可以访问Eric Lippert的博客(谷歌搜索即可找到),并使用联系链接。在那里提出你的问题或提供一个链接到这里。如果有人没有提供一个好的解释,他很可能会加入讨论。 - Anthony Pegram
简短的版本是,如果您调用MyMethod(1),则IL显示对MyMethod(int32)的调用,如果您使用MyMethod<int>(1),则为MyMethod<int32>,如果您使用MyMethod(1.0),则为MyMethod<float64>。我不是IL专家,但看起来解析的类型成为方法名称的一部分,消除了歧义。不过,我的理解可能完全错误。 - Anthony Pegram

2

1
实际上,该链接指向一个错误的错误报告。报告者只是没有理解显式方法在重载目的上胜过泛型。因此,虽然您可能有关于我们希望避免的复杂性的观点,但我认为这并不是由于任何实际的错误。 - Steven Sudit
1
人们遇到的问题是在有约束条件(不是签名的一部分!)或者有一个Reptile和一个Lizard:Reptile的情况下,当为Lizard lizard调用Foo<T>(T t)而不是Foo(Reptile reptile)时。原因是?泛型将编译成与Lizard的精确匹配,而不是使用与Reptile足够接近的匹配。 - Anthony Pegram
@Anthony:说得好。我猜想只有在我们将“Lizard”向上转型为“Reptile”之后,所期望的行为才可能发生,但这可能会更加微妙,就像我们处理int类型之间的隐式转换时一样。 - Steven Sudit

2

是的,它们可以。它们将允许像这样的代码:

A.MyMethod("a string"); // calls the generic version
A.MyMethod(42);  // calls the int version

1

是的。它们都有相同的名称"MyMethod",但具有不同的签名。然而,C#规范明确指出,当两者均可选时,编译器将优先选择非泛型版本而非泛型版本。


1

是的。就我所知,如果您调用 A.MyMethod(1);,它总会运行第二个方法。您需要调用 A.MyMethod<int>(1); 来强制运行第一个。


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