扩展方法
扩展方法有以下几个需求:
- 你想为一个类型添加一些 成员;
- 你不需要为类型的实例添加任何更多的数据;
- 你不能改变类型本身, 因为是别人的代码。
对于C#1和C#2中的静态方法,扩展方法是一种更优雅的解决方案。
语法
并不是任何方法都能作为扩展方法使用—— 它必须具有以下特征:
- 它必须在一个非嵌套的、 非泛型的静态类中( 所以必须是一 个静态方法);
- 它至少要有 一个参数;
- 第一个参数必须附加 this 关键字作为前缀;
- 第一个参数不能有其他任何修饰 符(比如out或ref);
- 第一个参数的类型不能是指针类型。
我们来试着给Stream类写一个扩展方法:
public static class StreamUtil { const int bufferSize = 8192; public static void CopyToo(this Stream inputStream, Stream outPutStream) { var buffer = new byte[bufferSize]; int read; while ((read=inputStream.Read(buffer,0,buffer.Length))>0) { outPutStream.Write(buffer,0,read); } } public static byte[] ReadFull(this Stream input) { using (MemoryStream stream=new MemoryStream()) { CopyToo(input,stream); return stream.ToArray(); } } }
扩展方法必须在顶级的静态类中进行声明,不能是嵌套的静态类。
扩展方法假装自己是另一个类的实例方法,来看看如何使用:
static void Main(string[] args) { WebRequest request = WebRequest.Create("https://www.baidu.com"); using (WebResponse response = request.GetResponse()) using (var resStream = response.GetResponseStream()) using(var outPut=File.Open(@"C:\Users\jianxin\Desktop\test.txt",FileMode.Open)) { resStream.CopyToo(outPut); } Console.WriteLine("done!"); Console.ReadKey(); }
之所以吧CopyTo改成CopyToo是因为Stream现在已经实现了这个扩展方法了。如果和实例方法同名,则不会去调用这个扩展方法。
一些原理
一般来说,如果你在一个对象后面调用这个对象的成员比如方法,编译器会首先从它的实例成员中去寻找,如果没有找到,他会在引入的命名空间里面去寻找合适的扩展方法。
为了决定是否使用 一个扩展方法, 编译器必须能区分扩展方法与某静态类中恰好具有合适签名的其他方法。 为此, 它会检查类和方法是否具有System.Runtime.CompilerServices.ExtensionAttribute 这个特性, 它是.NET 3. 5 新增的。 但是,编译器不检查特性来自哪个程序集。这意味着你可以在C#2或早前的版本中自己编写一个这个特性类来满足编译器的这种搜索策略。但是,谁特么还在用C#2或1呢?如果遇到多个合适的版本,还是会用“更好的选择”原则来选用最合适的那一个。
在空引用上面调用扩展方法
在空引用上面调用方法会导致NullRefrenceException的异常。但是可以调用扩展方法而不会导致异常。
public static class NullUtil { public static bool IsNull(this object obj) { return obj==null; } }
static void Main(string[] args) { object obj = null; Console.WriteLine(obj.IsNull());//true obj = new object(); Console.WriteLine(obj.IsNull());//false Console.ReadKey(); }
如果IsNull是一个实例方法那么会引发NullRefrenceException,但是扩展方法不会,可以试一试,很爽。这个写法与string.IsNullOrEmpty()形成了鲜明的对比。
Enumerable
LINQ差不多全部的功能都是用Enumerable和Queryable的扩展方法来得到的。
Enumerable中有一个不是扩展方法:Range
var collection = Enumerable.Range(0, 10); foreach (int item in collection) { Console.WriteLine(item); }
讲这个例子并不是因为它很特殊,是因为它的一个特性:延迟执行 。Range方法并不会真的构造含有适当数字的列表,它只是在恰当的时间生成那些数。 换言之,构造的可枚举的实例并不会做大部分工作。 它只是将东西准备好, 使数据能在适当的位置以一种“just-in-time” 的方式提供。 这称为延迟执行, 是LINQ的一个核心部分。
可以根据一个可枚举的实例返回另一个可枚举的实例,这在LINQ中是很常见的:collection.Reverse();
缓冲和流式技术
框架提供的扩展方法会尽量尝试对数据进行“ 流 式”(stream)或者说“管道”(pipe)传输。 要求一个迭代器提供下一个元素时, 它通常会从它链接的迭代器获取一个元素, 处理那个元素, 再返回符合要求的结果, 而不用占用自己更多的存储空间。 执行简单的转换和 过滤操作时, 这样做非常简单, 可用的数据处理起来也非常高效。 但是,对于某些操作来说, 比如反转或排序, 就要求所有数据都处于可用状态, 所以需要加载所有数据到内存来执行批处理。 缓冲和管道传输方式, 这两者的差别很像是加载整个DataSet读取数据和用 一个DataReader来每次处理一条记录的差别。 使用LINQ时务必想好真正需要的是什么, 一个简单的方法调用可能会严重影响性能。
流式传输(streaming) 也叫惰性求值(lazy evaluation),缓冲传输(bufferring)也叫热情求值(eager evaluation)。 例如,Reverse方法使用了延迟执行(deferred execution) , 它在第一次调用MoveNext之前不做任何事情。 但随后却热切地(eagerly) 对数据源求值。
惰性求值和热情求值都属于延迟执行的求值方式, 与立即执行(immediately execution)相对。 Stack Overflow上的一个帖子很好地阐述了它们之间的区别( 参见 http://stackoverflow.com/questions/2515796/deferred-execution-and-eager-evaluation)。
用where过滤并将方法调用链接在一起
where扩展方法是对集合进行过滤的一种简单但又十分强大的方式,看一下代码:
static void Main(string[] args) { var collection = Enumerable.Range(0, 10) .Where(x=>x%2!=0) .Reverse(); foreach (int item in collection) { Console.WriteLine(item); } Console.ReadKey(); }
上述代码使用where过滤掉了序列中的所有偶数,然后使用Reverse对序列进行了反转。希望你此时已经注意到了一个模式—— 我们将方法调用链接到一起 了。string.Replace()就是这样的一个模式。LINQ针对数据处理进行了专门的调整,将各个单独的操作链接在一起形成了一个管道,然后让信息在这个管道中流通。
有一个效率问题:上面的代码如果先调用Reverse再调用where的话会和之前的调用顺序的效率相同么?不会,Reverse必须计算出偶数,而偶数最终是要被抛弃的。而先用where过滤掉这部分数据后,Reverse要执行的计算明显变小了。
用select方法和匿名类型进行投影
Enumerable中最重要的投影方法就是select,它操纵一个IEnumerable<TSource>,把他转化成一个IEnumerable<TResult>.它利用了延迟执行的技术,只有在每个元素被请求时才真正的执行投影。
static void Main(string[] args) { var collection = Enumerable.Range(0, 10) .Where(x => x % 2 != 0) .Reverse() .Select(x => new { Original = x, SquareRoot = Math.Sqrt(x) }); foreach (var item in collection) { Console.WriteLine(item); } Console.ReadKey(); }
用OrderBy进行排序
在Linq中,一般是通过OrderBy或OrderByDescending。也可以继续排序。使用ThenBy和ThenByDescending。需要注意的就一点,排序不会改变原有集合,他会返回一个新的序列----LINQ操作符是无副作用 的:它们不会影响输入, 也不会改变环境。 除非你迭代的是一 个自然状态序列( 如从网络流中读取数据) 或使用含有副作用的委托参数。 这是函数式编程的方法, 可以使代码更加 可读、可测、可组合、可 预测、健壮并且线程安全。
GroupBy分组
假设要观察程序出现的bug的数量,对他们种类进行分组:
bugs.GroupBy(bug => bug.AssignedTo) .Select(list => new { Developer = list.Key, Count = list.Count() }) .OrderByDescending(x => x.Count);
结果是一个IGrouping<TKey, TElement>。 GroupBy有多个重载版本, 这里使用的是最简单的。 然后选择键(开发者的姓名) 和分配给他们的bug的数量。 之后,我们对结果进行排序, 最先显示分配到bug数量最多的开发者。
研究Enumerable类时, 往往会感觉搞不清楚具体发生的事情——例如,GroupBy的一个重载版本居然有4个类型参数和5个“普通”参数(3个是委托)。但是,不要惊慌——只要按照上一章描述的步骤慢慢梳理,将不同的类型赋给不同的类型参数, 直到清楚呈现出方法的样子。这样,理解起来就容易多了。 这些例子不是具体针对某个方法调用, 但我希望你能体会到将方法调用链接起来之后所发挥的巨大作用。 在这个链条中, 每个方法都获取一个原始集合, 并以某种形式返回另一个原始集合——中间可能过滤掉一些值,可能对它们进行排序,可能转换每一个元素, 可能聚合某些值, 或者做其他处理。 在许多情况下, 最终的代码都易读、易懂。在其他情况下, 它最起码也会比使用以前版本的C#写的等价代码简单得多。
使用思路和原则
流畅接口
因为扩展方法支持这种链式的调用。所以,才有了流畅接口的这个概念,比如OrderBy ThenBy等。就和自然语言一样。