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






4.86/5 (18投票s)
正确使用扩展方法的小窍门和技巧,以及我最喜欢的一些扩展方法
引言
扩展方法允许向现有类添加方法——某种程度上。然而,它们有一些常规方法所不具备的限制,并且有一些常规方法所无法实现的优点。本文将探讨这些区别,以及何时应该使用和不应该使用扩展方法。
背景
本文的目的不是从入门级解释扩展方法,而是作为快速总结。扩展方法是定义在静态类中的一个 `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
事实上,LINQ 正是这样——`IEnumerable
如何使用扩展方法
我真的不会深入探讨这个话题,因为它已经在其他地方得到了深入的介绍,但足以说,扩展方法应该是对现有类的一个相对简单的扩展,它具有与现有方法相似的风格和功能。您应该在方法名称和参数上使用与现有方法相同的命名约定,并且添加 `///` 注释至关重要,以便 Intellisense 可以帮助指导扩展方法的使用者。如果您发现您的扩展方法覆盖了不止一屏,那很可能不是一个好的扩展方法候选。
在 `string` 对象上,“`Left`”、“`Right`”、“`ToUpperIfNotNull`”等扩展方法通常被认为是 `string` 类的自然扩展。然而,名为“`IsAValidPhoneNumber`”的扩展方法则不适合作为扩展方法,此时您会正确地意识到需要一个外部验证类来处理。
我最喜欢的一些扩展……
多年来,我开发了相当多的扩展方法集合。其中一些我最常使用的已经展示在上面。使用正确定义的扩展方法,它会变得如此自然,以至于您会忘记它们不是内置的。是的,网上有很多这样的方法,但如果有人感兴趣,我可以按类别写一些文章来分解它们。这包括 `DateTime`、`string`、反射、`IEnumerable`、`XmlDocument`/`XDocument` 等。
历史
- 2015 年 11 月 17 日:初始版本