为什么我的静态方法会覆盖我的实例方法?

7
以下代码是否应该给出警告?
class Foo { public void Do() { /*...*/ } /*...*/ }
class Bar : Foo { public static void Do()  { /*...*/ } /*...*/ }

它会给出以下警告:

"警告 CS0108: 'Bar.Do()' 隐藏了继承成员 'Foo.Do()'。如果是有意隐藏,请使用 new 关键字。"

如果我对代码进行更改:

class Foo { public static void Do() { /*...*/ } /*...*/ }
class Bar : Foo { public void Do()  { /*...*/ } /*...*/ }

我也收到了同样的警告。

但是,如果我做出以下更改,警告就会消失。

class Foo { public void Do() { /*...*/ } /*...*/ }
class Bar : Foo { new public static void Do() { /*...*/ } /*...*/ }

让我再做进一步的修改:
class Foo { public void Do() { /*...*/ } /*...*/ }
class Bar : Foo { 
    new public static void Do() 
    { new Bar().Do();/*...*/ } /*...*/ 
}

以下代码无法编译:

“错误 CS0176:成员‘Bar.Do()’无法通过实例引用访问;请改用类型名称限定它。”

因此,我通过实例引用从静态方法中失去了对继承方法的访问权限!

这背后的逻辑是什么?或者是我哪里打错了?

我在尝试为我的派生自“Form”的表单定义一个静态方法“Show”时遇到了这个问题。

6个回答

6
您认为错误在哪里?事实上有警告是完全正确的。从C# 3.0规范,第10.3.4节可以看出:允许类成员声明声明具有与继承成员相同名称或签名的成员。当发生这种情况时,派生类成员被称为隐藏基类成员。隐藏继承的成员不被视为错误,但它会导致编译器发出警告。为了抑制警告,派生类成员的声明可以包括新修饰符,以指示派生成员旨在隐藏基础成员。
您的方法调用失败的原因比较微妙,但基本上是因为成员查找算法选择了静态方法,然后使用了7.5.5.1节中的以下部分:
验证所选最佳方法的最终验证: 在方法组的上下文中验证该方法: 如果最佳方法是静态方法,则该方法组必须是通过类型的简单名称或成员访问而导致的。如果最佳方法是实例方法,则方法组必须是通过变量或值、基数访问和简单名称而导致的。如果这两个要求都不满足,则会出现编译时错误。

1
嗨Jon, 点赞你的回答。 我认为,从语言用户的角度来看,bug就在于选择最佳调用方法的方式上 :-)。 对我而言,“new Bar().Do()”明确告诉了编译时正在调用哪个方法,没有必要产生错误! - isntn
1
我怀疑另一种选择是让语言变得更加复杂。通常会有一些边缘情况,如果语言更智能化,这些情况可能会得到改善 - 但这样做会使人更难以“了解”语言,并且在编译器中正确实现它也更加困难。这都是权衡的问题... - Jon Skeet
刚遇到这个问题(ExpressionVisitor:想要静态的Visit以及实例)。我认为它实际上简化了语言,因为它符合用户的期望 - 我们期望方法组应该根据上下文(类型或实例)进行过滤,并且期望编译器是聪明的。 - NetMage

2

不,这很有道理。这个可以按预期工作:

using System;
using System.Collections.Generic;

class Foo { public void Do() { /*...*/ } /*...*/ }
class Bar : Foo { 
    new public static void Do() 
    { ((Foo)new Bar()).Do();/*...*/ } /*...*/ 
}

这是因为编译器假设你有一个Bar类型,然后找到静态成员。通过将它转换为Foo(顺带一提),你使它在元数据中查找Foo(),一切都很好。


1
因为编译器不看实例,而是看类型元数据。在这里它只找到了一个正确签名的方法(名称和空参数列表),那就是静态方法。 - Lucero
考虑使用虚函数表来思考;Bar::Do() 映射隐藏了 Foo::Do() 的映射。 - Paul Sonier
嗨,Lucero,如果编译器查看类型元数据,那么类型元数据在编译期间是否可用?那么是谁首先从代码生成元数据的?编译过程中是否有不同的传递?McWafflesstix,如果您有时间,能否进一步阐述一下?提前致谢。 - isntn
元数据是由编译器生成的,我认为在内部编译器至少进行了两次遍历(因为声明的顺序不重要)。我的猜测是:词法分析、标记化、元数据/结构创建(这三个可以在一次遍历中完成),代码编译(第二次遍历)。 - Lucero
嗨,查尔斯,谢谢你的回答。根据你的说法,在v-table级别上,静态方法和实例方法之间没有区别,但我认为应该有区别。为什么没有呢?为什么v-table要这样做? - isntn
显示剩余5条评论

2
试试这个:
        new public static void Do()
        { 
            ((Foo)new Bar()).Do(); 
        }

1

你应该能够通过调用base.Do()来调用它,而不是Bar().Do()


0
在你的最终代码示例中,Bar.Do() 声明上的 new 关键字意味着你打算隐藏 Foo.Do()。

0

出于好奇,你为什么想要这样做?看起来你可能正在错误的解决方案上努力。

以上代码似乎按照预期工作,并且编译器消息告诉你问题所在。


嗨,乔希,我并不是“想要”这样做。只是我遇到了这个问题,并想要理解C#设计者选择的这种行为。"new Bar().Do()"显然表明正在调用实例方法。那么,为什么C#被设计成更加明确呢?这是一个有点教育意义的问题。 - isntn
我明白了。嗯,在这种情况下,你必须意识到在Bar中,没有名为Do()的实例方法,只有一个静态方法。通过给它相同的名称,你已经阻止了继承。因此,对于我或编译器来说,并不明显正在调用实例方法 =p。 - JoshJordan
我认为,在Bar中有一个从Foo派生的实例方法。 - isntn
这就是我的观点 - 这不是真的。假设你将它们都设置为非静态。那么,Bar.Do() 将会隐藏 Foo.Do()。也就是说,如果 Bar.Do() 存在,Foo.Do() 不会被继承。无论它是静态的还是实例方法,如果 Bar.Do() 存在,Foo.Do() 都不会被派生到 Bar 中。 - JoshJordan
我明白你的意思了,谢谢。换句话说,我想知道为什么这并不重要的逻辑原因。C#(或CLR)设计者有没有选择区分静态方法和实例方法是可选的?如果不是,为什么?如果是,为什么选择不区分?尽管我同意这并不是什么大问题。 - isntn

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