【问题标题】:What is the motivation behind "Use Extension Methods Sparingly?"“谨慎使用扩展方法”背后的动机是什么?
【发布时间】:2010-03-26 15:51:59
【问题描述】:

我发现它们是扩展现有类的一种非常自然的方式,尤其是当您只需要将某些功能“点焊”到现有类上时。

Microsoft 说:“一般来说,我们建议您谨慎地实施扩展方法,并且仅在必要时才实施。”然而,扩展方法构成了 Linq 的基础;事实上,Linq 是创建扩展方法的原因。

是否存在使用扩展方法优于继承或组合的特定设计标准?在什么条件下不鼓励他们?

【问题讨论】:

    标签: c# extension-methods


    【解决方案1】:

    我们向 C# MVP 展示提议的新语言特性(至少是那些有一半机会看到曙光的特性)以获取早期反馈。我们经常收到的关于许多功能的一些反馈是“我会根据该功能的设计原则明智地使用此功能,但我的傻瓜同事会对这个东西发疯,写出大量无法维护、无法理解的代码,我将在接下来的十年里一直在调试,所以无论我多么想要它,请永远不要实现这个功能!”

    虽然我对喜剧效果有些夸大,但这是我们非常重视的事情;我们希望新功能的设计能够鼓励它们的使用,但阻止它们的滥用。

    我们担心扩展方法会被用于,例如,不加选择地任意扩展“对象”,从而产生难以理解、调试和维护的松散类型泥浆。

    我个人特别关注您在“流利”编程中看到的“可爱”扩展方法。像

    这样的东西
    6.Times(x=>Print("hello"));
    

    哎呀。 “for”循环在 C# 中是普遍可以理解和惯用的; 别太可爱了。

    【讨论】:

    • 具有讽刺意味的是,该扩展方法可能会放在我的 .ForEach<T> 方法旁边!我发现它比 for 循环更容易理解且简洁得多,而且使用 i
    • @Chris:在这种情况下,只需使用 foreach。 Eric 的精彩帖子在这里解释了执行 ForEach - blogs.msdn.com/ericlippert/archive/2009/05/18/… 的问题
    • 然而,F# 包含Seq.iter,它完全等同于IEnumerable<T>.ForEach 扩展方法。我知道,不同的语言,不同的设计理念,但关于产生副作用的论点仍然可以应用,而且 F# 在功能上比 C# 更接近“纯”。
    【解决方案2】:

    问题是扩展方法不一定清楚。

    当你在代码中看到这样的内容时:

     myObject.Foo();
    

    本能地认为Foo()是定义在myObject类上的方法,或者是myObject类的父类。

    对于扩展方法,这不是真的。该方法几乎可以在任何地方定义,几乎可以用于任何类型(您可以在 System.Object 上定义扩展方法,尽管这是一个坏主意......)。这确实增加了代码的维护成本,因为它降低了可发现性。

    任何时候添加不明显的代码,甚至降低实现的“明显性”,都会产生长期可维护性方面的成本。

    【讨论】:

    • 右键选择“转到定义”有那么难吗?
    • @Mongus:不,不难。 - 但是您仍然可能在层次结构中的定义位置上弄错了。此外,如果您有与实例方法同名的扩展方法,则会增加混乱。避免使用它们的原因有很多——即使它们通常很有用。
    • 至少在打字时,智能感知通过智能感知菜单中的图标提供关于函数类型的指示符。
    • 仅仅通过查看代码中使用的实例方法,同样不可能知道该方法在继承层次结构中的位置。需要深入定义才能真正弄清楚这一点。如果我的扩展方法名称与实例方法名称相同,则 CLR 将始终调用实例方法。在这种情况下永远不能调用扩展方法。
    • @RBT 我认为您也应该谨慎使用继承;)
    【解决方案3】:

    过度使用扩展方法不好,原因与过度使用重载运算符不好。让我解释一下。

    让我们先看一下运算符重载的例子。当你看到代码时:

    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 中)。
    【解决方案4】:

    我遵循一个非常简单的扩展方法和辅助类规则。

    任何时候我有一个方法可以归为静态帮助类,我会在一般范围内权衡它的有用性,我真的希望共享这个方法吗?意思清楚且对其他人有用吗?

    如果我可以对这个问题回答是,我会将其创建为扩展方法。我有一个共享库,其中包含我在 1 个命名空间中的所有扩展方法,每个类文件都定义为 TypeNameExtension,因此很清楚该类中的预期内容,以便您可以轻松找到代码。

    如果我质疑是否需要将辅助方法作为全局可访问的用法,我将声明该方法为私有静态并将其保留在所属类中。

    【讨论】:

      【解决方案5】:

      当 Borland 发明它们(Delphi Class Helpers)时,指导方针已经是:“只应在特殊情况下使用”。

      它们已被广泛接受(作为 LINQ 的一部分),但它们仍然是一种奇怪的添加功能的方式。我认为 Reed 有一个很好的答案。

      【讨论】:

        【解决方案6】:

        我有一个不断增长的扩展方法库,这些扩展方法库对 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 (Between(test, minv, maxv)) {} 好吗?
        • @jmucchiello - 实际上是if (MathUtilities.Between(test, minv, maxv)) {}。所以你不得不问自己,MathUtilities 给了我什么价值,它是否影响了可读性?我认为答案是肯定的。这就是 Fluent 接口背后的重点。
        • @jmucchiello 它确实读起来更好,并且更清楚地知道什么之间会发生什么......
        • @Nick:扮演魔鬼的拥护者,我认为这真的很奇怪,并且完全符合您的语法:"Bar".IsBetween("Foo", "Baz"); - 通过指定 MathUtils,我至少知道我在做一些奇怪的事情......
        • @Reed - 为什么这很奇怪?如果您可以对字符串列表进行排序并按字母顺序排列,那么您真的可以说位于列表中两个字符串之间的另一个字符串是奇数吗?如果可以订购某些东西,那么自然会在其中两个之间有一些东西。
        【解决方案7】:

        扩展方法可以被认为是面向方面的。我相信微软是在说,如果你有源代码,你应该改变原来的类。它与清晰度和易于维护有关。需要扩展方法的原因是它们扩展了现有对象的功能。在 LINQ 的情况下,如果深入研究它的工作原理,可以使用无法提前知道的各种类型创建对象。使用扩展方法可以添加一些行为。

        例如,扩展 .NET 框架对象是有意义的,因为您没有源代码,但可能有一些行为要添加到对象中。扩展字符串、DateTime 和 FrameworkElement 类是可以接受的。为跨越应用程序边界的类创建扩展方法也是可以接受的。比如说,如果您想共享一个 DTO 类并拥有一些特定的 UI 行为,请仅在 UI 中创建该扩展方法。

        对于动态语言,范式是不同的,但我相信您说的是 C# 和 VB.net。

        【讨论】:

          猜你喜欢
          • 2010-10-02
          • 1970-01-01
          • 1970-01-01
          • 2015-04-30
          • 2017-01-26
          • 2011-02-07
          • 1970-01-01
          • 1970-01-01
          • 2010-09-29
          相关资源
          最近更新 更多