【发布时间】:2012-10-11 03:16:53
【问题描述】:
我一直在考虑使用扩展方法来替代抽象基类。扩展方法可以提供默认功能,并且可以通过将具有相同签名的方法放在派生类中来“覆盖”。
有什么理由我不应该这样做?
另外,如果我有两个具有相同签名的扩展方法,使用哪一个?有没有办法确定优先级?
【问题讨论】:
-
投票是因为我认为这是一个好问题,但却是个坏主意。
标签: c#
我一直在考虑使用扩展方法来替代抽象基类。扩展方法可以提供默认功能,并且可以通过将具有相同签名的方法放在派生类中来“覆盖”。
有什么理由我不应该这样做?
另外,如果我有两个具有相同签名的扩展方法,使用哪一个?有没有办法确定优先级?
【问题讨论】:
标签: c#
一般来说,您不应该通过扩展方法提供“基本”功能。它们应该只用于“扩展”类功能。如果您可以访问基类代码,并且您尝试实现的功能在逻辑上是继承层次结构的一部分,那么您应该将它放在抽象类中。
我的意思是,仅仅因为你可以并不意味着你应该。当普通的老式 OO 编程无法为您提供合理的解决方案时,最好坚持使用良好的老式 OOP 并使用较新的语言功能。
【讨论】:
这绝对是个坏主意。扩展方法是静态绑定的,这意味着除非您在编译时类型为子类型的对象上调用覆盖,否则您仍将继续调用扩展方法。再见多态性。 This page 对扩展方法的风险进行了很好的讨论。
【讨论】:
这里的所有答案都说“你不能”,就目前而言,这是正确的。大多数补充说“你不应该”。我想证明您应该能够做到——尽管这可能是一件小事。
举一个痛苦的现实世界的例子:如果你不幸使用了新的 MVC 框架,并且你的视图代码到处都在使用一些 HtmlHelper 扩展方法,并且你想覆盖它的默认行为......那么呢?
你是 SOL,就是这样。即使你做了“OOP 的事情”——从 HtmlHelper 派生,更改你的基本视图类以用你的 DerivedHtmlHelper 的实例替换“Html”对象实例,并在其中定义一个显式的“Foo”方法——即使你做了所有也就是说,调用“Html.Foo”将仍然调用原始扩展方法而不是方法。
这太令人惊讶了!毕竟,扩展方法只有在对象还没有方法的情况下才会被应用!这是怎么回事?
嗯,这是因为扩展方法是一个静态特性。也就是说,当看到 'Html.Foo' 时,编译器会查看 static 类型的 'Html'。如果它有一个 'Foo' 方法,它会像往常一样被调用。否则,如果有 'SomeClass' 提供了 'Foo' 扩展方法,它会将表达式转换为 'SomeClass.Foo(Html)'。
您会期望编译器会考虑对象的 dynamic 类型。也就是说,生成的(伪)代码将读取 'Html.HasMethod("Foo") ? Html.Foo() : SomeClass.Foo(Html)'。
这当然会产生在每个扩展方法调用中使用反射的成本。因此,您会期望您可以编写类似 'static void Foo(virtual this HtmlHelper html)' 的内容来显式请求编译器插入运行时检查。将此称为“虚拟扩展方法”。
然而,在他们有限的预算和无限的智慧中,C# 语言设计者只选择了更高效、更受限制的替代方案。当我需要覆盖 HtmlHelper 的默认行为时,这让我仍然 SOL :-(
【讨论】:
我同意迈克尔的观点。基类应该包含所有基本功能扩展方法显然应该扩展基本功能。在像 Ruby 这样的动态语言中,通常使用扩展方法来提供附加功能而不是使用子类。基本上,扩展方法是用子类代替,而不是用基类代替。
我见过的唯一例外是,如果您有多个类型具有不同的类层次结构(如 winform 控件),您可以创建每个都实现和接口的子类,然后扩展该接口,从而给出将“基础”功能应用于一组不同的控件,而无需扩展控件或对象之类的所有内容。
编辑:回答你的第二个问题
我认为编译器会为你捕捉到这个。
【讨论】:
你有充分的理由不应该这样做。第一个是,你不能保证你的扩展将如何被调用:
MyExtensions.AMethod(myObj)
或
myObj.AMethod()
第二个只是第一个的语法糖。
您的建议与语言功能的精神背道而驰。扩展方法绝对不是面向对象的。然而,您正在尝试实现一种面向对象的技术。不要为此使用扩展方法。
【讨论】:
它们在语义上是不同的操作。例如,多态性可能无法像抽象基类那样工作。
作为一般规则,使用任何语言工具来实现其设计目的。扩展方法不是继承的替代品,它们是一种使用(通常)已经可见的接口来扩展类功能的技术。
【讨论】:
如果您不需要唯一的派生类,您可能需要考虑上述多态性。
来自此 MSDN article 的一些一般准则:
在类中定义方法后,任何扩展方法 共享该方法名称的类将不再执行。
扩展方法由命名空间加载。为了让用户 其他扩展方法,您可能应该避免将 与被扩展的类在同一命名空间中的扩展方法。
如果不同的依赖程序集使用同一个库,每个程序集都需要针对库中的类的自定义函数,您可以在该程序集的命名空间中声明特定于依赖程序集的扩展。不依赖于该程序集的其他程序集将没有该扩展名。
虽然这不是多态,但您可能不想使用多态,因为它需要所有派生类实现它们的覆盖。
换句话说,如果您有具有特定变体逻辑的派生词,请使用多态性。 如果您只是在特定情况下需要自定义函数,否则可能会造成混乱或繁重,那么扩展方法不是一个坏主意。
另外,请参阅 JoshRivers 的回答,其中展示了如何使用单独命名空间中的类中的命名空间覆盖扩展方法。
【讨论】:
首先,检查 [new] 作为方法修饰符会很好 - 它应该提供相同的性能和方法分离,但麻烦少得多。
如果在那之后你还在思考同样的技巧,这里是要考虑的主要选项 - 不涉及意识形态,只列出你需要注意的方面:-)
首先,您可能必须将所有使用限制为模板化函数参数以保证确定性函数选择,因为此技巧将创建与 CLR 保证跨函数调用边界确定性绑定优先级的情况完全相反的情况。
如果您仍然要拥有抽象基类并且您拥有所有代码,那么仅仅将一个基方法“转换”为扩展根本不会给您带来任何好处,只会让您失去一个仍然存在的通用接口其他一切,包括 vtables 的成本。
如果您的目标是将抽象类转换为具体类,出于性能原因消除所有虚拟方法并仅在模板中使用具体基类和所有派生类(甚至可能转换为结构),那么该技巧可以为您带来一些性能。您只需要注意,如果不进行重构,您将无法回到基于接口的使用。您还需要使用签名中的基类测试非模板函数中的调用顺序,如果您有多个 dll-s,请格外小心。用 [new] 运算符试试同样的方法,看看效果是否更好。
此外,您必须为每个派生类编写单独的方法实现,即使它们具有完全相同的功能,所以如果您有很多派生类,您会看到大量代码重复。即使您使用 [new],这部分也会对您产生影响,除非您返回到虚拟方法并在继承中引入另一层 - 这当然会取消所有性能增益。
【讨论】:
添加指向另一个 SO 问题的链接,该问题的答案不是“你不能”。更多“你不能……除非”。
【讨论】: