我知道覆盖和新的区别(或者相信这样做无论如何),并且有几个问题描述了两者之间的差异,但我的问题是,是否有一个特殊的原因,为什么C#默认为新的行为(有警告),而不是默认覆盖?
public class Base
{
virtual public string GetString() => "Hello from Base";
}
public class Child : Base
{
public string GetString() => "Hello from Child";
}
...
var childAsBase = (Base)new Child();
Console.WriteLine(childAsBase.GetString());
...
c:\>dotnet run
child.cs(5,23): warning CS0114: 'Child.GetString()' hides inherited member 'Base.GetString()'.
To make the current member override that implementation, add the override keyword.
Otherwise add the new keyword. [C:\IPutAllMyProjectsInMyRootFolder.csproj]
Hello from Base
我可以认为,无论继承的方法是否标记为虚拟,都可以获得相同的行为,但与此同时,将虚拟声明为“#34;覆盖我"所以默认覆盖对我来说似乎是合理的。
我想到的另一个原因是使用虚函数表的成本,但这似乎是一个可怕的原因,因为我作为编码器希望代码执行的操作应该比保存cpu-cycles更重要。但也许在发明语言时,情况并非如此?
答案 0 :(得分:9)
当涉及类型层次结构的C#语言设计决策对您来说似乎不太常见时,一个好的技巧就是问自己问题"如果有人在没有告诉我的情况下改变我的基类会怎么样?" C#经过精心设计,旨在降低脆弱的基类失败的成本,而且这是一个。
让我们首先考虑一种阴影方法具有override
关键字的情况。
这向编译器表明派生类作者和基类作者正在合作。基类作者制作了一个可覆盖的方法,这是超级危险的事情。一个可覆盖的方法意味着你不能编写一个测试该方法的所有可能行为的测试用例!方法的可覆盖性必须在中设计,因此你需要说一个方法是虚拟的(或抽象的)。
如果我们看到override
修饰符,那么我们就知道基类和派生类作者都对这个危险扩展点的正确性和安全性负责,并且已经成功地相互沟通以达成一致意见。合同。
让我们接下来考虑一种阴影方法具有new
关键字的情况。同样,我们知道派生类作者已经检查了基类,并且确定了阴影方法,无论是否为虚拟方法,都不能满足派生类消费者的需求,并且故意做出了两个具有相同签名的方法的危险决定。
然后,我们留下了阴影方法既没有override
也没有new
的情况。 我们没有证据表明派生类的作者知道基类中的方法。事实上,我们有相反的证据;如果他们知道虚拟基类方法,他们会覆盖它以匹配虚方法的合同,如果他们知道非虚基类方法,那么他们会故意制作影响它的危险决定。
这种情况怎么会出现?只有两种方式可以想到。
首先,派生类作者没有充分研究他们的基类,并且不知道他们只是隐藏的方法的存在,这是一个可怕的要在其中的位置。派生类继承基类的行为,并且可以在需要维护基类的不变量的场景中使用!我们必须警告无知的开发人员他们正在做一些非常危险的事情。
其次,在更改基类之后,重新编译派生类。现在,派生类作者并不知道基类,因为它是原始编写的,并且因为他们设计了他们的派生类,并且他们测试了他们的派生类。但他们不知道基类发生了变化。
同样,我们必须警告无知的开发人员发生了一些事情,他们需要做出以下重要决定:尽可能覆盖,或者确认隐藏,或重命名或删除派生类方法。
这证明了为什么在阴影方法既不标记new
也不标override
时必须发出警告的原因。但这不是你的问题。您的问题是"为什么默认为new
?"
好吧,假设您是编译器开发人员。当编译器面临缺少new
和override
的阴影方法时,您可以选择以下选项:
什么都不做;不发出任何警告或错误,并选择一种行为。如果代码由于脆弱的基类失败而中断,那就太糟糕了。您应该更仔细地查看基类。显然,我们可以做得更好。
将其设为错误。现在,基类作者可以通过更改基类的成员来打破您的构建。这不是一个糟糕的想法,但我们现在必须权衡期望的构建中断的成本 - 因为他们发现了一个错误 - 与不需要的构建中断的成本相比 - 需要默认行为的地方 - 意外忽略警告并引入错误的成本。
这是一个棘手的问题,各方都有争论。引入警告是一个合理的妥协立场;你可以随时打开"警告是错误",我建议你这样做。
但是让我们暂时不谈。如果可能,自动覆盖的其他后果是什么?请记住,场景的前提是覆盖是偶然的,派生类作者不了解基类的实现细节,不变量和公共表面区域。
自动更改基类方法的所有调用方的行为似乎极其危险与仅更改的行为的危险相比,只有那些调用阴影的调用者方法通过派生类型的接收器。
所有设计选择都是仔细权衡许多互不相容的设计目标的结果。 C#的设计人员特别关注大型团队从事版本化软件组件的工作,其中基类可能以意想不到的方式发生变化,团队可能无法将这些变化很好地传达给彼此。
我想到的另一个原因是使用虚函数表的成本,但这似乎是一个可怕的原因,因为我作为编码器希望代码执行的操作应该比保存cpu-cycles更重要。但也许在发明语言时,情况并非如此?
虚拟方法引入成本;显而易见的成本是运行时额外的表跳转以及获取它所需的代码。还有一些不太明显的成本,例如:抖动不能对非密封方法进行内联虚拟调用,等等。
但是正如您所注意到的那样,将非虚拟化作为默认值的原因主要不是为了提高性能。主要原因是虚拟化非常危险,需要仔细设计。必须由覆盖方法的派生类维护的不变量需要记录和传达。正确设计类型层次结构昂贵,并使其选择性降低成本并提高安全性。坦率地说,我希望密封也是默认的。