覆盖扩展方法

24

我一直在考虑使用扩展方法来替代抽象基类。扩展方法可以提供默认功能,并且可以通过在派生类中放置具有相同签名的方法来“重写”它们。

是否存在任何不应该这样做的理由?

此外,如果我有两个具有相同签名的扩展方法,哪一个会被使用?是否有一种确定优先级的方法?


2
投一票赞成,因为我认为这是一个好问题,但不是一个好主意。 - Jay Bazuzi
9个回答

12

一般来说,不应通过扩展方法提供“基础”功能。它们只应用于“扩展”类功能。如果您可以访问基类代码,并且您正在尝试实现的功能在逻辑上属于继承层次结构的一部分,则应将其放在抽象类中。

我的观点是,仅仅因为你可以并不意味着你应该这样做。通常最好坚持传统的面向对象编程(OOP),当平凡的OO编程不能提供合理的解决方案时再使用新的语言特性。


8

这绝对是一个不好的想法。扩展方法是静态绑定的,这意味着除非您在编译时类型为子类型的对象上调用覆盖,否则您仍将继续调用扩展方法。再见多态性。此页面对扩展方法的危险进行了很好的讨论。


是的,这是一个问题。我尝试添加一个扩展方法到Control类中,以提供一个新方法的默认实现,然后计划在我想要提供特定行为的几个控件中进行子类化。但是基类引用(到Control)不会调用子类型版本。我原本期望“真正”的方法总是会覆盖扩展方法,即使在子类型上也是如此,但事实并非如此。就像你所说的一样。 - Jan Hettich
然而,我能够以相当不错的方式解决这个问题。由于我正在向子类化控件添加的方法属于一个新的自定义接口,客户端代码可以首先尝试将 Control 的实例转换为新接口,如果接口不受支持,则调用扩展方法。 - Jan Hettich

4
所有回答都说“不行”,就事论事而言这是正确的。大多数人还补充道“你也不应该这么做”。但我认为,即使只有微不足道的安慰,你也应该能够实现这一点。
举个令人痛苦的真实例子:如果你不幸正在使用新版的MVC框架,并且你的视图代码在各处都使用了HtmlHelper扩展方法,并且你想要覆盖其默认行为...那么怎么办呢?
无解,什么也做不了。即使你按照“面向对象编程(OOP)的方法”——从HtmlHelper派生,更改你的基础视图类以用DerivedHtmlHelper的实例替换“Html”对象实例,并在其中定义一个显式的“Foo”方法——即使你做了所有这些,调用“Html.Foo”仍然会调用原始的扩展方法而非方法本身。
这很让人惊讶!毕竟,我们只期望扩展方法在对象没有自己的方法时使用!这里到底发生了什么?
嗯,这是因为扩展方法是一种静态特性。也就是说,当看到“Html.Foo”时,编译器查看“Html”的静态类型。如果它有一个“Foo”方法,它将像往常一样被调用。否则,如果有“SomeClass”提供了一个“Foo”扩展方法,它会将表达式转换为“SomeClass.Foo(Html)”。
你期望的是编译器会考虑对象的动态类型。也就是说,生成的(伪)代码将读取“Html.HasMethod("Foo") ? Html.Foo() : SomeClass.Foo(Html)” 。
当然,这会在每次扩展方法调用中产生使用反射的开销。因此,你可以写类似于“static void Foo(virtual this HtmlHelper html)”的内容,以明确请求编译器插入运行时检查。称之为“虚拟扩展方法”。
然而,在他们有限的预算和无穷的智慧中,C#语言设计者只选择了更有效、更受限制的替代方案。这让我仍然无解,当我需要覆盖HtmlHelper的默认行为时 :-(

3
我同意Michael的观点。基类应包含所有基本功能。扩展方法显然是要扩展基本功能的。在像Ruby这样的动态语言中,通常使用扩展方法来提供额外的功能,而不是使用子类。基本上,扩展方法用于替换使用子类,而不是替换使用基类。
我见过的唯一例外是,如果您有多个类型具有不同的类层次结构(如winform控件),则可以为每个类型创建一个子类,所有子类都实现并扩展该接口,从而为一组不同的控件提供“基本”功能,而不是扩展Control或Object等所有内容。
编辑:回答您的第二个问题
我认为编译器会为您捕获此问题。

当你说扩展方法是用来替换子类而不是基类时,我不确定你的意思。因为编译器更喜欢本地实现而不是扩展实现,所以扩展方法不就是“基类”方法吗? - JoshRivers
我的意思是,通常情况下,如果您正在使用具有类Foo的程序集,并需要添加DoSomething方法,则会创建一个SubFoo子类,其具有DoSomething方法。使用扩展方法,您可以通过创建扩展方法来替换子类的需要。 - Jacob Adams

2

它们是语义上不同的操作。例如,多态性可能无法像使用抽象基类那样工作。

通常情况下,使用任何语言工具都要按照其设计目的使用。扩展方法并不是继承的替代品,而是一种使用(通常是已经可见的)接口来扩展类功能的技术。


2

有很多原因你不应该这样做。首先,你无法保证如何调用你的扩展:

MyExtensions.AMethod(myObj)

或者

myObj.AMethod()

第二个仅仅是对第一个的语法糖。
你提出的建议违背了语言特性的精神。扩展方法显然不是面向对象的,而你正在试图实现一种面向对象的技术。不要使用扩展方法来达到这个目的。

执行语法为什么很重要? “功能的精神”不是哲学论点吗?SOLID原则并不总是“面向对象”的。有时候你需要违反“规则”才能得到一个好的设计? - JoshRivers
你所建议的是一个hack,它会反噬你。你正在与语言作斗争。你试图攀登成功的紧绷绳索,并在失败的深渊上行走。请不要这样做。 - yfeldblum

2

如果您不需要独特的派生类,那么您可能想考虑放弃多态性。

以下是来自 MSDN 文章 的一些通用指南:

  1. 一旦在类中定义了一个方法,任何共享该方法名称的该类的扩展方法将不再执行。

  2. 扩展方法按命名空间加载。为了允许其他扩展方法的用户,您应该避免将扩展方法放在与被扩展的类相同的命名空间中。

如果各个依赖程序集正在使用同一库,并且每个程序集都需要针对库中的类进行自定义函数,则可以在该程序集内的命名空间中声明特定于依赖程序集的扩展。不依赖于该程序集的其他程序集将没有该扩展。

虽然这不是多态性,但您可能不想使用多态性,因为它要求所有派生类实现其重写。

换句话说,如果您有具有特定变体逻辑的派生类,请使用多态性。 如果您只是需要在特定情况下使用自定义功能,否则可能会令人困惑或繁琐,那么扩展方法并不是一个坏主意。

另外,可以参考JoshRivers的答案,展示如何使用一个类内部的命名空间覆盖扩展方法。

0

添加一个链接到另一个SO问题,该问题有一个与“你不能”不同的备选答案。更多的是“你不能……除非”。

如何覆盖现有的扩展方法


0

首先,检查[new]作为方法修饰符可能是个不错的选择 - 它应该提供相同的性能和方法分离,但会减少很多麻烦。

如果在那之后你仍在考虑同样的技巧,下面是要考虑的主要选项 - 没有涉及意识形态,只是列举需要注意的方面 :-)

首先,您可能需要将所有使用限制为模板化函数参数,以保证确定性函数选择,因为这个技巧将创建与CLR在函数调用边界上保证确定性绑定优先级完全相反的情况。

如果您仍然要使用抽象基类,并且您拥有所有代码,那么仅将一个基本方法“转换”为扩展将一无所获,而且还会使您失去一个共同接口,这个接口仍将存在于其他所有内容中,包括vtable的成本。

如果你的目标是将抽象类转换为具体类,出于性能原因消除所有虚方法,并在模板中仅使用具体基类和所有派生类(甚至转换为结构体),那么这个技巧可以帮助你提高性能。你只需要意识到,一旦进行了重构,就无法回到基于接口的使用方式。你还需要测试非模板函数中带有基类签名的调用顺序,并且如果你有多个dll,则需要特别小心。尝试使用[new]运算符进行相同的操作,看看是否效果更好。

此外,即使它们具有完全相同的功能,你也必须为每个派生类编写单独的方法实现,因此如果你有很多派生类,你将面临大量的代码重复。即使你使用[new],这部分也会影响你,除非你回到虚方法并在继承中引入另一层 - 这当然会取消所有性能收益。


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