为什么我们需要new关键字,为什么默认行为是隐藏而不是覆盖?


81

我在看这篇博客文章时有以下问题:

  • 我们为什么需要new关键字,仅仅是指定隐藏一个基类方法。我的意思是,为什么我们需要它?如果我们不使用override关键字,我们是否隐藏了基类方法?
  • 为什么C#中的默认值是隐藏而不是覆盖?设计者为什么要以这种方式实现它?

6
(对我而言)一个更有趣的问题是,为什么隐藏是完全合法的。它通常是一个错误(因此出现编译器警告),很少被使用,并且充其量是有问题的。
康拉德·鲁道夫

Answers:


127

好问题。让我重申一下。

为何完全隐藏另一个方法是合法的?

让我用一个例子回答这个问题。您有CLR v1的接口:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

超。现在,在CLR v2的你有仿制药,你认为“男人,如果在V1恨不有仿制药,我会做这个的通用接口。但我没有。我应该做的东西,现在与它兼容即通用的,所以这我得到了泛型的好处,而又不会失去与期望IEnumerable的代码的向后兼容性。”

interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> .... uh oh

您将如何调用GetEnumerator方法IEnumerable<T>?请记住,您希望它在非通用基本接口上隐藏GetEnumerator。除非您明确处于向后兼容的情况,否则您永远都不希望调用该东西。

仅此一点就证明了方法的隐藏。有关方法隐藏的理由的更多想法,请参阅我关于该主题的文章

为什么没有“新”的隐藏会引起警告?

因为我们希望引起您注意,所以您隐藏了某些东西,并且可能是意外地在做。请记住,您可能是由于别人对基类的编辑而不是您对派生类的编辑而意外隐藏了某些东西。

为什么隐藏没有“新”内容的警告而不是错误?

相同的原因。您可能会意外隐藏了某些东西,因为您刚刚获得了新版本的基类。这事儿常常发生。FooCorp创建基类B。BarCorp使用方法Bar创建派生类D,因为他们的客户喜欢该方法。FooCorp看到了这一点,然后说,嘿,这是个好主意,我们可以将该功能放在基类上。他们这样做并发布了新版本的Foo.DLL,当BarCorp选择新版本时,如果告诉他们他们的方法现在隐藏了基类方法,那就太好了。

我们希望这种情况只是警告而不是错误,因为使其成为错误意味着这是脆性基类问题的另一种形式。C#经过精心设计,以便当有人对基类进行更改时,对使用派生类的代码的影响减至最小。

为什么隐藏而不覆盖默认值?

因为虚拟覆盖很危险。虚拟覆盖允许派生类更改已编译为使用基类的代码的行为。做一些危险的事情,例如做一个超越,应该是您有意识地有意地做的,而不是偶然的。


1
FWIW,我不同意“仅凭帽子就可以隐藏方法。” 有时最好打破向后兼容性。Python 3表明,这实际上可以有效地工作而不会破坏太多的工作。重要的是要注意,“不惜一切代价向后兼容”并不是唯一合理的选择。这个原因可以说到另一点:改变基类总是很脆弱的。实际上,打破依赖版本可能更可取。(但对于在实际问题下方的评论回答了该问题,+ 1。)
Konrad Rudolph 2010年

@Konrad:我同意向后兼容会造成伤害。Mono已决定在将来的版本中放弃对1.1的支持。
simendsjo 2010年

1
@Konrad:打破与您的语言的向后兼容性与使使用您的语言的人轻松打破与自己的代码的向后兼容性大不相同。埃里克的例子是第二种情况。编写该代码的人已经决定要向后兼容。如果可能,该语言应支持该决定。
布赖恩2010年

上帝回答,埃里克。但是,存在一个更具体的原因,即默认情况下覆盖无效是很糟糕的。如果基类Base引入了一个新方法Duck()(返回鸭子对象),而该方法恰好与派生类Derived中的现有方法Duck()(使Mario duck)具有相同的签名(即,您和《 Base》的作者),您希望这两个类别的客户在只想要Duck时都制作Mario Duck。
jyoungdev 2010年

总之,它主要与更改基类和接口有关。
jyoungdev 2010年

15

如果派生类中的方法前面带有new关键字,则该方法被定义为独立于基类中的方法

但是,如果您既不指定new也不覆盖,则输出结果与指定new相同,但是会收到编译器警告(因为您可能不知道自己在基类方法中隐藏了一个方法,甚至确实您可能想覆盖它,而只是忘记了包含关键字)。

因此,它可以帮助您避免错误,并明确显示您想做什么,并使代码更具可读性,因此您可以轻松地理解您的代码。


12

值得注意的是,在这种情况下,唯一的作用new就是抑制警告。语义没有变化。

因此,一个答案是:我们需要new向编译器发送信号,告知其隐藏是故意的,并摆脱了警告。

后续问题是:如果您不会/无法覆盖一个方法,为什么要引入另一个同名方法?因为隐藏本质上是名称冲突。您当然会在大多数情况下避免使用它。

我能想到的故意隐藏的唯一好理由是,当某个接口将名称强加给您时。


6

在C#中,成员默认情况下是密封的,这意味着您无法覆盖它们(除非用virtualabstract关键字标记),并且出于性能原因这是必需的。该new修饰符是用来明确隐藏继承成员。


4
但是,何时真正要使用新的修饰符?
simendsjo 2010年

1
当您想显式隐藏从基类继承的成员时。隐藏继承的成员意味着该成员的派生版本将替换基类版本。
Darin Dimitrov

3
但是,任何使用基类的方法仍将调用基实现,而不是派生。对我来说,这似乎是一种危险的情况。
simendsjo 2010年

@ simendsjo,@ Darin Dimitrov:我也很难考虑确实需要使用新关键字的用例。在我看来,总有更好的方法。我还想知道,隐藏基类实现是否不是容易导致错误的设计缺陷。
德克·沃尔玛

2

如果默认覆盖而不指定override关键字,则由于名称相等,您可能会意外覆盖基础的某些方法。

.Net编译器策略是为了安全起见而发出警告,只是为了安全起见,因此在这种情况下,如果默认覆盖,则每个覆盖方法都必须发出警告-类似于“警告:检查您是否真正想要覆盖”。


0

我的猜测主要是由于多接口继承。使用谨慎的接口,两个不同的接口很有可能使用相同的方法签名。允许使用new关键字将允许您使用一个类创建这些不同的实现,而不必创建两个不同的类。

已更新... Eric给了我一个有关如何改进此示例的想法。

public interface IAction1
{
    int DoWork();
}
public interface IAction2
{
    string DoWork();
}

public class MyBase : IAction1
{
    public int DoWork() { return 0; }
}
public class MyClass : MyBase, IAction2
{
    public new string DoWork() { return "Hi"; }
}

class Program
{
    static void Main(string[] args)
    {
        var myClass = new MyClass();
        var ret0 = myClass.DoWork(); //Hi
        var ret1 = ((IAction1)myClass).DoWork(); //0
        var ret2 = ((IAction2)myClass).DoWork(); //Hi
        var ret3 = ((MyBase)myClass).DoWork(); //0
        var ret4 = ((MyClass)myClass).DoWork(); //Hi
    }
}

1
使用多个接口时,可以显式命名接口方法,以避免冲突。我不明白您所说的“对一个类的不同实现”是什么意思,它们都具有相同的签名...
simendsjo 2010年

new关键字允许您也更改所有从该点子孙的产业基地。
Matthew Whited 2010年

-1

如前所述,方法/属性隐藏使更改方法或属性方面的事情成为可能,而这些事情原本无法轻易更改。一种可能有用的情况是允许继承的类具有读写属性,这些属性在基类中是只读的。例如,假设基类具有一堆名为Value1-Value40的只读属性(当然,真实的类将使用更好的名称)。此类的密封后代具有一个构造函数,该构造函数采用基类的对象并从该对象复制值。此后,该类不允许更改它们。一个不同的,可继承的后代声明了一个称为Value1-Value40的读写属性,该属性在读取时的行为与基类版本相同,但是在写入时允许写入值。

这种方法的一个烦人之处-也许有人可以帮助我-是我不知道一种在同一类中同时遮盖和覆盖特定属性的方法。是否有任何CLR语言允许(我使用vb 2005)?如果基类对象及其属性可以是抽象的,这将很有用,但是这将要求中间类在后代类将其遮蔽之前覆盖Value1到Value40属性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.