65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展方法:理解其优势与正确用法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (18投票s)

2015年11月17日

CPOL

6分钟阅读

viewsIcon

26634

正确使用扩展方法的小窍门和技巧,以及我最喜欢的一些扩展方法

引言

扩展方法允许向现有类添加方法——某种程度上。然而,它们有一些常规方法所不具备的限制,并且有一些常规方法所无法实现的优点。本文将探讨这些区别,以及何时应该使用和不应该使用扩展方法。

背景

本文的目的不是从入门级解释扩展方法,而是作为快速总结。扩展方法是定义在静态类中的一个 `static` 方法,其第一个参数前有一个 `'this'` 关键字,用于指定它正在扩展的类类型。一个简单的例子就能清楚地说明这一点。

//
// Declaring the extension method
//

public static class MyExtensions
{
     public static bool IsWellDefined(this string str)
     {
          return !string.IsNullOrWhiteSpace(str);
     }
}

//
// Using the extension method
//

public class Testing
{
    public void DoStuff(string name)
    {
        if (!name.IsWellDefined())
           throw new ArgumentException(...)
        else
           ...
    }
}

扩展方法的缺点和优点

缺点

命名空间引用

也许使用扩展方法最大的缺点是,您需要确保包含定义扩展方法的静态类的 `namespace` 在文件的开头包含在“`using`”子句中(除非静态类已位于您正在处理的类的 `namespace` 层次结构中)。初次使用扩展方法时,这可能会令人困惑,因为一个方法会出现在一个文件中的 Intellisense 中,而在另一个文件中不出现,即使 Intellisense 清楚地标记它是扩展方法,也可能被忽略。

然而,对于许多系统来说,这并不是一个主要问题,因为通常会有一个核心功能程序集,它通常是每个文件的一部分。

因此,如果您将所有常用的“好东西”都保留在程序集 `MyCompany.Core.dll` 中,并且该程序集几乎在每个源代码文件中都通过“`using MyCompany.Core`”进行了引用,那么您只需将扩展方法类放在 `namespace MyCompany.Core` 中,它们几乎就可以从任何地方使用。

缺少“ref”能力

不允许将“`ref`”标签定义在扩展方法的与“`this`”参数相同的参数上。因此,您不能定义一个扩展方法来执行此操作。

public void foo()
{
     int value = 3;
     value.IncrementBy2(); // having an extension method like this to turn value to 5 isn't possible
}

我还没有试验过 7.0 中新的“ref”功能以及它们如何与扩展方法结合使用。看看它们如何相互作用会很有趣。

优点

第三方类

这很简单,但您可能正在使用第三方来源的程序集,您没有直接修改类的选项,但您有一些简单的功能可以以符合扩展方法用法的方式进行重用(见下文),因此添加扩展方法的能力可以大大提高代码的可读性。

空引用

记住,扩展方法实际上只是“语法糖”,在编译时,实际调用的是合适的 `static` 方法,因此所有常规 `static` 方法调用的规则都适用。特别是,您可以处理 `null` 值。考虑上面“背景”部分中的示例。如果您调用 `DoStuff(null)`,您可能会期望“`if`”语句会抛出对象引用未找到异常,但它不会。这会被转换为 `if (MyExtensions.IsWellDefined(null))`,其中传入 `null` 值是完全有效的。能够引用 `null` 值可以带来各种有用的扩展方法。例如,我的库中有一个名为 `ToUpperIfNotNull()` 的扩展方法,当输入值为 `null` 时返回 `null`,在不为 `null` 时转换为大写。类似地,我有一个这样的扩展方法:

public static string ToString(this DateTime? dt, string format)
{
     return dt.HasValue ? dt.Value.ToString(format) : null;
}

在经常出现 `null` 检查的情况下,这种扩展方法语法可以使您的代码更加简洁。

减少异常检查/处理

抛出异常是编写良好代码的自然组成部分,但我们经常会遇到一些常见情况,我们希望避免异常处理而不使代码复杂化。这是一个常见示例。如果您超出边界区域,`string.Substring` 方法将抛出异常。因此,以下代码将抛出异常:

string newValue = "hello".Substring(0, 10);

当然,您永远不会直接这样做,但更通用的用法会是这样的:

public static string GetLeftCharacters(string str, int numChars)
{
     if (str != null && str.Length >= numChars)
        return str.Substring(0, numChars);
     else
        return str;
}

但由于这可能经常出现,一个更好的解决方案是将其转换为扩展方法,只需稍作签名更改:

public static string Left(this string str, int numChars)
{
     if (str != null && str.Length >= numChars)
        return str.Substring(0, numChars);
     else
        return str;
}

这让我们回到了 VB 的美好时光,现在我们可以这样做,而无需担心抛出异常。

public void DoSomething(string str)
{
     if (str.Left(3) == "ABC")
        ...
     else
        ...
}

另一个非常常见的问题是:

public static void DoSomething(IDictionary<int, string> myDictionary)
{
     // Method 1: this is clean, but does 2 lookups

     string twoValue = myDictionary.ContainsKey(2) ? myDictionary[2] : "defaultValue";

     // Method 2: one lookup, but messy

     string outValue;
     if (!myDictionary.TryGetValue(2, out outValue))
        outValue = "defaultValue";

     // Method 3: using the extension method below

     string thisTwoValue = myDictionary.GetValueOrDefault(2, "defaultValue");
}

上面的 `GetValueOrDefault` 扩展方法不仅更简洁,而且使用与 .NET 中其他地方使用的命名约定相似的命名约定来扩展 `dictionary`,这使其更棒。

public static VAL GetValueOrDefault<KEY, VAL>(this IDictionary<KEY, VAL> dict, 
                                              KEY key, VAL defaultValue)
{
    VAL val = default(VAL);
    return (dict != null && key != null && dict.TryGetValue(key, out val)) ? val : defaultValue;
}

这使我讲到了关于扩展方法的最后一个优点……

接口上的扩展方法

任何纯粹主义者都会告诉你,即使 C# 支持将方法定义放入接口的概念,这也是错误的,错误的,错误的。嗯,总的来说,我认为这些论点是有道理的,但在某些情况下,稍微违反这个规则可以带来巨大的好处。

上面的最后一个例子就是一个很好的证明。当然,我可以直接在 `Dictionary` 上定义 `GetValueOrDefault` 扩展方法,但这有什么好处呢?记住,扩展方法是对 `static` 方法调用的语法糖,并且在几乎所有情况下,具有接口参数比具体类更可取。因此,在许多情况下,在接口上使用扩展方法比具体的类等效方法更可取(在我看来……)。

事实上,LINQ 正是这样——`IEnumerable` 上的扩展方法!

如何使用扩展方法

我真的不会深入探讨这个话题,因为它已经在其他地方得到了深入的介绍,但足以说,扩展方法应该是对现有类的一个相对简单的扩展,它具有与现有方法相似的风格和功能。您应该在方法名称和参数上使用与现有方法相同的命名约定,并且添加 `///` 注释至关重要,以便 Intellisense 可以帮助指导扩展方法的使用者。如果您发现您的扩展方法覆盖了不止一屏,那很可能不是一个好的扩展方法候选。

在 `string` 对象上,“`Left`”、“`Right`”、“`ToUpperIfNotNull`”等扩展方法通常被认为是 `string` 类的自然扩展。然而,名为“`IsAValidPhoneNumber`”的扩展方法则不适合作为扩展方法,此时您会正确地意识到需要一个外部验证类来处理。

我最喜欢的一些扩展……

多年来,我开发了相当多的扩展方法集合。其中一些我最常使用的已经展示在上面。使用正确定义的扩展方法,它会变得如此自然,以至于您会忘记它们不是内置的。是的,网上有很多这样的方法,但如果有人感兴趣,我可以按类别写一些文章来分解它们。这包括 `DateTime`、`string`、反射、`IEnumerable`、`XmlDocument`/`XDocument` 等。

历史

  • 2015 年 11 月 17 日:初始版本
© . All rights reserved.