方法隐藏是一个好的想法吗?

39
在 C# 中,new修饰符可用于隐藏基类方法而不覆盖基类方法。
我从未遇到过隐藏方法是最佳选择的情况。是否存在方法隐藏是最佳选择的情况?

返回类型协变显然是常见的用例。事实上,我遇到过那种情况不止一次,并且认为它是不可能完成的。现在我知道得更清楚了。控件中的属性隐藏是一个不错的小技巧。Eric Lippert的GST示例很有逻辑意义,但偏离了常见的面向对象编程习惯,因此我会非常谨慎地使用它。 - ScottS
8个回答

21

使用方法隐藏的情况很少,但有时确实非常有效。Eric Lippert在他的博客上发布了一个很好的例子

interface IEnumerable<T> : IEnumerable { 
  new IEnumerator<T> GetEnumerator(); 
}

然而,我认为隐藏应该是例外,只有在必要时才应该使用。


2
是的,但Eric Lippert后来评论说:“当然,如果C#支持方法返回类型协变性,这就不必是一个新方法了。这为方法隐藏提供了更大的好处;它允许在没有此类功能的语言中实现类似于返回类型协变性的东西。”。那么问题是:为什么C#不支持返回类型协变性? - ceztko
哦,忘了:其实我有一个使用“new”的例子,我认为比GetEnumerator()更好。 - ceztko

19

我有时候为了方便调用者而使用它:

abstract public class Animal { }

public class Mouse : Animal { }



public class AnimalTrap
{
    public Animal TrappedAnimal { get; }
}

public class MouseTrap : AnimalTrap
{
    new public Mouse TrappedAnimal
    {
        get { return (Mouse)base.TrappedAnimal; }
    }
}
所以,当派生类保证被捕获的动物总是一只老鼠时,调用者就不必自己转换为 Mouse 类型。并且多态功能仍然存在。

2
这是一个很好的返回类型协变的示例,正如Eric Lippert的博客文章所讨论的那样。我不得不停下来思考IEnumerable<T>的例子,但这个例子很简单明了。 - ScottS
2
@ScottS:这是一个很好的方法隐藏示例,但与返回类型协变无关。实际上,在这里没有类型安全性保证,调用者可以将MouseTrap强制转换回AnimalTrap并放入不同类型的Animal - Aaronaught
1
@ScottS:协变返回类型的概念适用于重写方法——如果您可以重写基本方法,但返回比基类更具体的类型,则意味着语言允许协变返回类型。(顺便说一句,C#不支持协变返回类型,尽管它支持其他类型的协变/抗变。)方法隐藏不是协变——您可以使用“new”声明一个方法并返回任何类型。 - Aaronaught
上面的例子可以很容易地改为通用并使用类型参数。结果:一只老鼠永远是一只老鼠。这使得在此示例中使用“new”关键字变得多余。我仍然认为,“new”从来不是一个好选择,因为有更好的方法。 - Krumelur
如果您可以控制AnimalTrap的源代码,为什么不创建一个通用的class AnimalTrap<TAnimal> where TAnimal : Animal { ... }呢? 如果您真的想要一个非泛型的动物陷阱,您可以像这样继承泛型类:class AnimalTrap : AnimalTrap<Animal> { ... } - Nullius
显示剩余3条评论

14
通常,在创建自定义控件并希望防止某些属性出现在设计器中时,这是一个不错的选择。有时属性是不可覆盖的,但设计器并不关心这一点,它只关心最低级公共属性是否有[Browsable]属性。
例如,假设您的控件不支持填充。Control.Padding属性不可重写。但是,您也知道如果有人设置填充,什么“坏事”也不会发生,只是该属性不起任何作用,因此您不想让用户在设计器中看到它,并认为它实际上有效。因此,将其隐藏:
public class MyControl : Control
{
    [Browsable(false)]
    public new Padding Padding
    {
        get { return base.Padding; }
        set { base.Padding = value; }
    }
}
在这种情况下,我们实际上是使用成员隐藏来隐藏成员 - 从设计者的角度看不到。
另外,我知道有其他方法可以实现这个目标 - 这只是一个可能的选择。

我一直在使用这种方法来隐藏一个属性。你所说的其他方法是什么?你有链接吗? - Pierre-Alain Vigeant
@Pierre:你也可以实现自己的 TypeDescriptorTypeConverter。该方法的弱点(或优点,取决于您的需求)是它是选择加入的,而成员隐藏版本是选择退出的。 - Aaronaught

6
我能想到的最常见的例子是像DbCommandSqlCommand这样的东西;具体类型(SqlCommand等)通常会进行大量方法隐藏,以使属性/方法的返回类型显示正确的实现类型。这是因为相关对象本身具有附加的(特定于实现的)功能,调用者不想每次调用都进行强制转换。

4

如果你只是基类的使用者,而新版本的基类突然发布了一个与你在派生类中已经实现的方法具有完全相同签名的方法,则需要能够隐藏基类方法,并使用 new 来明确表明你正在隐藏基类方法。


1
这是我提出问题时知道的唯一合法用途。我会尽力更改方法名称,避免使用 new 关键字。如果我无法更改名称,我可能会引入一个新名称的方法,并将冲突的方法标记为已弃用,同时添加 new 关键字来隐藏基本方法。 - ScottS
1
比基类添加具有相同名称和签名的方法的可能性更糟糕的是,基类添加具有不同签名的同名方法的可能性。如果DerivedFoo有一个DoSomething(double)方法,并且Foo的未来版本添加了一个DoSomething(int)方法,则代码可能会期望derivedThing.DoSomething(4)调用派生类方法,但是基类中的新重载可以被认为是“更好的匹配”。并非所有这个问题的变化都会导致编译器诊断。 - supercat

1
我最近遇到了一个情况,其中“new”关键字非常有用。我正在为遗留代码库中的方法编写单元测试,并且我要测试的一些类中的某些方法具有隐藏在方法内部的外部依赖项。当时,包括一个模拟框架来解决问题似乎是不必要的,因此,我继承了我想要测试的类到单元测试类中,而不是使用模拟框架。然而,父方法不是虚拟的,因此无法被覆盖。我的第一个解决方案是简单地向原始方法添加虚拟关键字,以允许其被覆盖,测试效果很好。即使它起作用,但我认为仅因需要在单元测试中“模拟”而向方法签名添加虚拟关键字并不是一个好的设计决策(但我可能错了)。相反,在子类中使用'new'关键字使我能够通过使用具有静态返回值的方法隐藏原始方法来“摆脱”父方法的依赖性,从而编写快速的单元测试,这样做的效果与更改非虚拟方法为虚拟方法时一样好。
总的来说,我不确定这种技术能否被视为良好的实践。然而,在编写测试套件以便于重构使用多个依赖关系和没有自动化测试套件的旧代码时,使用这种技术可能非常有用。在重构工作完成后,测试很可能会得到改进。
编辑:因为方法隐藏仅发生在私有继承类中,所以不应该引起其他用例可能出现的混淆。

你的回答几乎逐字逐句地读懂了我的想法。这可能不是最佳实践,但这似乎以一种周到的方式解决了一个真正的问题。我同意在原始方法中添加虚拟关键字并不是一个好主意,因为你正在通过低级模拟类的实现来驱动更高级别、更抽象类的设计。 - Nicholas Miller

1

我曾经不得不使用方法隐藏。我正在使用一个第三方库,该库提供了一个非虚拟方法(这是整个问题的关键)。它是一个分配某些东西实例的方法。但是我需要扩展该方法的功能。

所以我使用了 'new' 关键字来隐藏基类实现,并在派生类中提供自己的实现。但这并不意味着我没有调用基类方法。事实上,我确实调用了基类方法,但我在我的方法中做了其他的事情,在最后一切都很好。

因此,我认为原因如下:

  1. 基类在你无法控制的第三方库中。
  2. 基类有一个你需要重写的非虚拟方法。
  3. 始终特别注意扩展而不是替换功能。
  4. 当被卡住时,始终将其作为最后的手段.... :)

仅供参考...


0

我曾经在自定义的winforms控件中使用过它。我的自定义控件覆盖了Text属性,每当它改变时,我想触发一个事件。基本的Control类带有一个TextChanged事件,但是这个基本事件上有一些属性,以防止它出现在设计器或者智能感知中。由于我们在自定义控件中使用该事件,这是不可取的,因此我们采取以下措施:

    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    public new event EventHandler TextChanged
    {
        add { base.TextChanged += value; }
        remove { base.TextChanged -= value; }
    }

无论使用何种类型的引用来访问它,事件处理程序仍将添加到同一事件中,但派生类型的引用将在智能感知中显示TextChanged事件,并且当我们的控件放置在设计器中的表单上时,TextChanged事件将显示在属性窗口中。

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