【发布时间】:2010-03-26 15:51:59
【问题描述】:
我发现它们是扩展现有类的一种非常自然的方式,尤其是当您只需要将某些功能“点焊”到现有类上时。
Microsoft 说:“一般来说,我们建议您谨慎地实施扩展方法,并且仅在必要时才实施。”然而,扩展方法构成了 Linq 的基础;事实上,Linq 是创建扩展方法的原因。
是否存在使用扩展方法优于继承或组合的特定设计标准?在什么条件下不鼓励他们?
【问题讨论】:
标签: c# extension-methods
我发现它们是扩展现有类的一种非常自然的方式,尤其是当您只需要将某些功能“点焊”到现有类上时。
Microsoft 说:“一般来说,我们建议您谨慎地实施扩展方法,并且仅在必要时才实施。”然而,扩展方法构成了 Linq 的基础;事实上,Linq 是创建扩展方法的原因。
是否存在使用扩展方法优于继承或组合的特定设计标准?在什么条件下不鼓励他们?
【问题讨论】:
标签: c# extension-methods
我们向 C# MVP 展示提议的新语言特性(至少是那些有一半机会看到曙光的特性)以获取早期反馈。我们经常收到的关于许多功能的一些反馈是“我会根据该功能的设计原则明智地使用此功能,但我的傻瓜同事会对这个东西发疯,写出大量无法维护、无法理解的代码,我将在接下来的十年里一直在调试,所以无论我多么想要它,请永远不要实现这个功能!”
虽然我对喜剧效果有些夸大,但这是我们非常重视的事情;我们希望新功能的设计能够鼓励它们的使用,但阻止它们的滥用。
我们担心扩展方法会被用于,例如,不加选择地任意扩展“对象”,从而产生难以理解、调试和维护的松散类型泥浆。
我个人特别关注您在“流利”编程中看到的“可爱”扩展方法。像
这样的东西6.Times(x=>Print("hello"));
哎呀。 “for”循环在 C# 中是普遍可以理解和惯用的; 别太可爱了。
【讨论】:
.ForEach<T> 方法旁边!我发现它比 for 循环更容易理解且简洁得多,而且使用 i
Seq.iter,它完全等同于IEnumerable<T>.ForEach 扩展方法。我知道,不同的语言,不同的设计理念,但关于产生副作用的论点仍然可以应用,而且 F# 在功能上比 C# 更接近“纯”。
问题是扩展方法不一定清楚。
当你在代码中看到这样的内容时:
myObject.Foo();
本能地认为Foo()是定义在myObject类上的方法,或者是myObject类的父类。
对于扩展方法,这不是真的。该方法几乎可以在任何地方定义,几乎可以用于任何类型(您可以在 System.Object 上定义扩展方法,尽管这是一个坏主意......)。这确实增加了代码的维护成本,因为它降低了可发现性。
任何时候添加不明显的代码,甚至降低实现的“明显性”,都会产生长期可维护性方面的成本。
【讨论】:
过度使用扩展方法不好,原因与过度使用重载运算符不好。让我解释一下。
让我们先看一下运算符重载的例子。当你看到代码时:
var result = myFoo + myBar;
您希望myFoo 和myBar 是数字类型,而operator + 正在执行加法。这是合乎逻辑的,直观的,易于理解的。但是一旦运算符重载出现,您就无法再确定myFoo + myBar 实际在做什么 - + 运算符可能被重载为意味着什么。您不能只阅读代码并弄清楚发生了什么而不必阅读表达式中涉及的类型的所有代码。现在,operator + 已经为String 或DateTime 之类的类型重载了——然而,在这些情况下addition 的含义有一个直观的解释。更重要的是,在那些常见的情况下,它为代码增加了很多表达能力。因此,潜在的混乱是值得的。
那么这一切与扩展方法有什么关系呢?好吧,扩展方法引入了类似的情况。没有扩展方法,当你看到:
var result = myFoo.DoSomething();
您可以假设DoSomething() 是myFoo 的一种方法或它的一个基类。这很简单,易于理解,甚至直观。但是使用扩展方法,DoSomething() 可以在任何地方定义 - 更糟糕的是,定义取决于代码文件中的一组 using 语句,并可能引入许多类,任何一个其中可以承载DoSomething()的实现。
现在不要误会我的意思。 运算符重载和扩展方法都是有用且强大的语言特性。但请记住 - 能力越大,责任越大。当这些特性提高了实现的清晰度或功能时,您应该使用它们。如果您开始不加选择地使用它们,它们会给您尝试创建的内容添加混乱、复杂性和可能的缺陷。
【讨论】:
But with extension methods, DoSomething() could be defined anywhere - and worse, the definition depends on the set of using statements in the code file and bring in potentially many classes into the mix, any one of which could host the implementation of DoSomething() - 只需右键单击并从上下文菜单中选择转到其定义即可解决此问题(在 Visual Studio 中)。
我遵循一个非常简单的扩展方法和辅助类规则。
任何时候我有一个方法可以归为静态帮助类,我会在一般范围内权衡它的有用性,我真的希望共享这个方法吗?意思清楚且对其他人有用吗?
如果我可以对这个问题回答是,我会将其创建为扩展方法。我有一个共享库,其中包含我在 1 个命名空间中的所有扩展方法,每个类文件都定义为 TypeNameExtension,因此很清楚该类中的预期内容,以便您可以轻松找到代码。
如果我质疑是否需要将辅助方法作为全局可访问的用法,我将声明该方法为私有静态并将其保留在所属类中。
【讨论】:
当 Borland 发明它们(Delphi Class Helpers)时,指导方针已经是:“只应在特殊情况下使用”。
它们已被广泛接受(作为 LINQ 的一部分),但它们仍然是一种奇怪的添加功能的方式。我认为 Reed 有一个很好的答案。
【讨论】:
我有一个不断增长的扩展方法库,这些扩展方法库对 BCL 类进行操作,以简化常见问题,乍一看似乎令人困惑。坦率地说,我认为扩展方法使事情更具可读性。例如,使用 if 语句测试一个数字是否介于两个数字之间:
if (x <= right && x >= left )
{
// Do Something
}
如果right 意外小于left 怎么办?那究竟是做什么的?当变量只是像x 和left 时,那么它很容易理解。但是如果它们是长类的属性呢? if 语句可能会变得非常大,这会混淆您正在尝试做的相当简单的事情。
所以我写了这个:
public static bool Between<T>(this T test, T minValue, T maxValue)
where T : IComparable<T>
{
// If minValue is greater than maxValue, simply reverse the comparision.
if (minValue.CompareTo(maxValue) == 1)
return (test.CompareTo(maxValue) >= 0 && test.CompareTo(minValue) <= 0);
else
return (test.CompareTo(minValue) >= 0 && test.CompareTo(maxValue) <= 0);
}
现在我可以将我的 if 语句改进为:
if (x.Between(left, right))
{
// Do Something
}
这是“非凡”吗?我不这么认为...但我确实认为它在可读性方面有了显着改进,并且它允许对测试输入进行一些额外的检查以确保安全。
我认为微软想要避免的危险是人们编写了大量的扩展方法来重新定义 Equals 和 ToString。
【讨论】:
if (MathUtilities.Between(test, minv, maxv)) {}。所以你不得不问自己,MathUtilities 给了我什么价值,它是否影响了可读性?我认为答案是肯定的。这就是 Fluent 接口背后的重点。
扩展方法可以被认为是面向方面的。我相信微软是在说,如果你有源代码,你应该改变原来的类。它与清晰度和易于维护有关。需要扩展方法的原因是它们扩展了现有对象的功能。在 LINQ 的情况下,如果深入研究它的工作原理,可以使用无法提前知道的各种类型创建对象。使用扩展方法可以添加一些行为。
例如,扩展 .NET 框架对象是有意义的,因为您没有源代码,但可能有一些行为要添加到对象中。扩展字符串、DateTime 和 FrameworkElement 类是可以接受的。为跨越应用程序边界的类创建扩展方法也是可以接受的。比如说,如果您想共享一个 DTO 类并拥有一些特定的 UI 行为,请仅在 UI 中创建该扩展方法。
对于动态语言,范式是不同的,但我相信您说的是 C# 和 VB.net。
【讨论】: