《深入理解C#》整理6-扩展方法
C# 3引入了扩展方法的概念,它既有静态方法的优点,又使调用它们的代码的可读性得到了提高。使用扩展方法,可以像调用实例方法那样调用静态方法。
一、扩展方法的语法
1、声明扩展方法
并不是任何方法都能作为扩展方法使用——它必须具有以下特征:
- 它必须在一个非嵌套的、非泛型的静态类中(所以必须是一个静态方法);
- 它至少要有一个参数;
- 第一个参数必须附加this关键字作为前缀;
- 第一个参数不能有其他任何修饰符(比如out或ref);
- 第一个参数的类型不能是指针类型。
我们将第一个参数的类型称为方法的扩展类型(extended type),即指该方法扩展了该类型。
2、调用扩展方法
编译器已将扩展方法调用转换成对普通静态方法的调用。调用时,会将调用方的值作为第一个实参的值传递。
3、扩展方法是怎样被发现的
①如果使用using指令,扩展方法可以像类一样不加限制地在代码中使用。如果编译器认为一个表达式好像是要使用一个实例方法,但没有找到与这个方法调用兼容的实例方法,就会查找一个合适的扩展方法。它会检查导入的所有命名空间和当前命名空间中的所有扩展方法,并匹配那些从表达式类型到扩展类型存在着隐式转换的扩展方法。
②为了决定是否使用一个扩展方法,编译器必须能区分扩展方法与某静态类中恰好具有合适签名的其他方法。为此,它会检查类和方法是否具有System.Runtime.CompilerServices.ExtensionAttribute这个特性(它是.NET 3.5新增的)。但是,编译器不检查特性来自哪个程序集。这意味着即使你的项目面向的是.NET 2.0,仍然可以使用扩展方法——只需在正确的命名空间中使用正确的名称来定义自己的属性就可以了。然后,你可以声明扩展方法,该特性会自动应用到方法和类上。编译器还会将该特性应用到包含扩展方法的程序集上。
③如果存在多个适用的扩展方法,它们可应用于不同的扩展类型(使用隐式转换),那么将使用在重载的方法中应用的“更好的转换”规则,来选择最合适的方法。要注意的一个重点是,如果存在适当的实例方法,则实例方法肯定会先于扩展方法使用。但是,编译器不会警告你存在一个和现有的实例方法匹配的扩展方法。
④扩展方法应用于代码的方式还存在一个潜在的问题——它的应用范围过于宽泛。如果同一个命名空间中的两个类含有扩展类型相同的方法,就没办法做到只用其中一个类中的扩展方法。
4、空引用上调用方法
在C#中,你不能在空引用上调用实例方法,但你可以在空引用上调用扩展方法。在C# 3中,扩展方法可以和扩展类型的一个现有的静态方法具有相同的签名
二、.NET 3.5中的扩展方法
扩展方法最大的用途就是为LINQ服务。Enumerable和Queryable是两个类特别醒目的的扩展方法,两者都在System.Linq命名空间中。在这两个类中,含有许许多多的扩展方法:Enumerable的大多数扩展的是IEnumerable
1、从Enumerable开始起步
在Enumerable中,有几个方法不是扩展方法,如Range方法,它获取两个int参数:一个起始数,一个是要生成的结果的数目。结果是一个IEnumerable
框架提供的扩展方法会尽量尝试对数据进行“流式”(stream)或者说“管道”(pipe)传输。要求一个迭代器提供下一个元素时,它通常会从它链接的迭代器获取一个元素,处理那个元素,再返回符合要求的结果,而不用占用自己更多的存储空间。执行简单的转换和过滤操作时,这样做非常简单,可用的数据处理起来也非常高效。但是,对于某些操作来说,比如反转或排序,就要求所有数据都处于可用状态,所以需要加载所有数据到内存来执行批处理。缓冲和管道传输方式,这两者的差别很像是加载整个DataSet读取数据和用一个DataReader来每次处理一条记录的差别。使用LINQ时务必想好真正需要的是什么,一个简单的方法调用可能会严重影响性能。
2、用Where过滤并将方法调用链接到一起
Where扩展方法是对集合进行过滤的一种简单但又十分强大的方式:它接受一个谓词,并将其应用于原始集合中的每个元素。Where同样返回一个IEnumerable
3、用Select方法和匿名类型进行投影
Enumerable中最重要的投影方法就是Select——它操纵一个IEnumerable
4、用OrderBy方法进行排序
LINQ操作符是无副作用的:它们不会影响输入,也不会改变环境。因而OrderBy排序不会改变原有集合——它返回的是新的序列,所产生的数据与输入序列相同,当然除了顺序。这与List
三、使用思路和原则
1、扩展世界”和使接口更丰富
对于一个给定的问题,程序员通常习惯于构建一个解决方案,直到最终能满足需求。现在,我们可以扩展世界来迎合解决方案,而不是一直构建方案,直到最终满足需求。如果一个库没有提供你需要的,就扩展这个库来满足你的需求。
2、流畅接口
在框架中,流畅接口的一个很好的例子就是OrderBy和ThenBy方法:用Lambda表达式稍加诠释,代码准确地描述了它要做的事情。这样的语句能像完整的英文句子那样读,而不是由独立的“名词动词化”的短语构成。
3、理智使用扩展方法
- 将扩展方法放到它们自己的命名空间,可有效防止被误用。
- 在扩展广泛使用的类型(如数字、object等)之前,或编写扩展类型实际为类型参数这样的扩展方法之前,要深思熟虑。
- 写扩展方法应该始终是一个有意识的决定,不要把它培养成一个习惯。绝对不是每个静态方法都该变成一个扩展方法。
- 在文档中指出第一个参数(在该值上调用扩展方法)是否允许为null——如果不允许,就在方法中检查值,并在必要的时候抛出一个异常(Argument Null Exception)。
- 如果方法名已经在扩展类型中使用,就不要再使用这个名称。如果扩展类型是框架中的类型,或者来自某个第三方库,请在库的版本发生改变时检查自己的所有扩展方法。
- 将应用于同一个扩展类型的扩展方法分组到一个静态类中。有的时候,相关的类(比如DateTime和TimeSpan)的扩展方法可以分组到一起。但是,如果扩展方法作用于迥然不同的类型(比如Stream和string),就不要把它们分组到同一个类中了。
- 在两个不同的命名空间中添加名字相同、扩展类型也相同的两个扩展方法时一定要三思,尤其是在两个方法都适用(它们有相同数量的参数)的情况下。合理的做法是添加或删除一个using指令,就可以使程序构建失败。但是,即使添加或删除一个using指令,程序也能构建,只是行为可能已经发生了变化①,这样就比较烦人了。