为什么在C#中允许重写静态方法

10
protected static new void WhyIsThisValidCode()
{
}

为什么允许覆盖静态方法?这样做只会带来错误,并且它的工作方式并不像你想象的那样。

看下面的类。

class BaseLogger
{
    protected static string LogName { get { return null; } }

    public static void Log(string message) { Logger.Log(message, LogName); }
}

class SpecificLogger : BaseLogger
{
    protected static string LogName { get { return "Specific"; } }
}

这是允许的,且代码正确

SpecificLogger.Log("test");

alt也是被允许的,但它并不能根据代码表现出来的样子做到你想要的效果。

它会使用LogName = null来调用Logger.Log方法。

那么为什么允许这样做呢?


13
正如其他人所说,这并不是覆盖 - 它做的正是我所期望的。请不要假定每个人都和你想法一样。 - Jon Skeet
6个回答

26

new关键字不会覆盖方法,而是创建一个与原始方法同名的新方法,这个新方法是独立的。无法重写静态方法,因为它们不是虚拟的。


2
我有时会想念C#中的虚拟类方法。但它们可能不够常用,不值得将它们添加到语言中。 - CodesInChaos

15

你并不是覆盖它,而是隐藏它。普通方法会表现出完全相同的行为,所以这里没有任何特定于静态方法的东西。

隐藏只在一些情况下有用。我遇到最常见的情况之一是在派生类中使返回类型更加具体化。但我从未在静态方法中遇到过这种情况。

静态函数在某个名称下可能有用的一个领域是,如果您使用反射并希望通过从方法返回来获取每个类的信息。但是当然,在大多数情况下,属性更适合。

你的代码会产生编译器警告,因此不太可能引起错误:

警告 'StaticHiding.SpecificLogger.LogName' 隐藏了继承的成员 'StaticHiding.BaseLogger.LogName'。如果想要进行隐藏,请使用 new 关键字。

如果您使用 new,那么您应该知道自己在做什么。


12
其他人指出这并不是覆盖,但这仍然留下了你最初的问题:为什么你能够这样做?(但问题实际上是“为什么你能隐藏静态方法”)。
这是支持包含基类和使用这些基类的组件的独立版本的不可避免的特性。
例如,想象一下,组件CompB包含基类,而另一个组件CompD包含一个派生类。在CompB的第1个版本中,可能没有名为LogName的属性。CompD的作者决定添加一个名为LogName的静态属性。
在这一点上,关键的是要理解CompD的第1个版本的作者并不打算替换或隐藏基类的任何特性 - 当他们编写那段代码时,基类中没有名为LogName的成员。
现在想象一下,CompB库发布了一个新版本。在这个新版本中,作者添加了一个LogName属性。在CompD中应该发生什么?选项似乎是:
CompD不再工作,因为它引入的LogName与添加到CompB的LogName发生冲突。
以某种方式使CompD的LogName替换基础CompB的LogName。(实际上,这如何与静态成员一起工作并不明显。不过,对于非静态成员,你可以设想这一点。)
将这两个LogName成员视为完全不同的成员,只是恰好具有相同的名称。(实际上,它们并不相同 - 它们被称为BaseLogger.LogName和SpecificLogger.LogName。但由于在C#中我们不总是需要用类限定成员名称,所以看起来它们是相同的名称。)
.NET选择执行第3个选项(无论是静态还是非静态)。如果您想要第2个行为-替换-与非静态一起使用,则基类必须是virtual并且派生类必须将该方法标记为override,以明确表示它们有意覆盖了基类中的方法。除非派生类明确声明希望这样做,否则C#永远不会使派生类的方法替换基类的方法。这很可能是安全的,因为这两个成员之间没有关联-基类的LogName甚至在引入派生类时都不存在。与仅仅因为基类的最新版本引入了新成员而导致的简单中断相比,这是更可取的。
如果没有这个功能,新版本的.NET Framework将无法向现有的基类添加新成员,而不会导致破坏性的改变。
你说行为不符合你的期望。实际上,这正是我所期望的,也是你在实践中可能想要的。BaseLogger并不知道SpecificLogger引入了自己的LogName属性。(因为你不能重写静态方法,所以没有机制可以知道。)当SpecificLogger的作者编写LogName属性时,请记住他们是针对没有LogName的BaseLogger的v1版本编写的,所以他们并不打算替换基本方法。既然两个类都不想要替换,显然替换是错误的选择。
唯一可能导致你陷入这种情况的场景是这两个类位于不同的组件中。(显然,你可以构造一个场景,让它们位于同一个组件中,但为什么要这样做呢?如果你拥有这两段代码并将它们发布在一个单独的组件中,这样做是疯狂的。)因此,BaseLogger应该有自己的LogName属性,这正是发生的情况。你可能写过这样的代码:
SpecificLogger.Log("test");

但是C#编译器发现SpecificLogger没有提供Log方法,所以将其转换为:
BaseLogger.Log("test");

因为这是Log方法定义的地方。
所以,每当你在派生类中定义一个方法,而不是试图覆盖现有的方法时,C#编译器会在元数据中指示这一点。(方法元数据中有一个"newslot"设置,表示这个方法是全新的,与基类中的任何内容无关。)
但是,如果你想重新编译CompD,这会给你带来问题。假设你收到了一个由于某个完全不相关的代码问题而导致的错误报告,并且你需要发布CompD的新版本。你将其编译为新版本的CompB。如果你编写的代码是不允许的,实际上你将无法编译 - 已经编译的旧代码可以工作,但你将无法编译那些代码的新版本,这有点疯狂。
因此,为了支持这种(实际上有些晦涩)的情况,他们允许你这样做。他们会生成一个警告,让你知道这里有一个命名冲突。你需要使用"new"关键字来消除它。
这是一个晦涩的情景,但如果你想要支持跨组件边界的继承,你就需要这个功能,否则在基类上添加新的公共或受保护成员将不可避免地引发破坏性变更。这就是为什么这个功能存在的原因。但是依赖它是不好的实践,所以你会得到一个编译器警告。使用new关键字来消除警告只应该是一个权宜之计。
底线是:这个功能只存在一个原因,那就是在某个基类的新版本添加了一个之前不存在的成员,并且与你的派生类上已有的成员冲突时,帮助你摆脱困境。如果你不处于这种情况下,请不要使用这个功能。
(我认为他们实际上应该在你省略new时发出错误而不是警告,以使这一点更加清晰。)

2

静态方法和静态字段不属于类实例,而是属于类定义。静态方法不参与虚拟分派机制,也不是虚拟方法表的一部分。

它们只是特定类上的方法和字段。

尽管你可以使用SpecificLogger.Log()这样的语法,看起来好像方法和字段被“继承”了,但那只是为了让你不必一直引用基类。

静态方法和字段实际上只是全局方法和字段,只不过是面向对象的那种。


1

令我惊讶的是,在.net Framework 4.5.1和VS 2013下,以下代码被允许且无误。

class A
{
    public static void Foo()
    {
    }
}

class B : A
{

}

class Program
{
    static void main(string[] args)
    {
        B.Foo();
    }
}

0

你没有覆盖基类中的属性,而是隐藏了它。在运行时使用的实际属性取决于你正在使用哪个接口。以下示例说明:

SpecificLogger a = new SpecificLogger();
BaseLogger b = new SpecificLogger();

Console.Write(a.Log); // Specific
Console.Write(b.Log); // null

在你的代码中,Log方法实际上是针对BaseLogger接口进行操作的,因为Log方法是BaseLogger类的一部分。
静态方法和属性无法被覆盖,当你想要隐藏一个属性时,应该使用"new"关键字来表示你正在隐藏某些内容。

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