扩展方法

扩展方法有以下几个需求:

  • 你想为一个类型添加一些 成员;
  • 你不需要为类型的实例添加任何更多的数据;
  • 你不能改变类型本身, 因为是别人的代码。

对于C#1和C#2中的静态方法,扩展方法是一种更优雅的解决方案。

语法

并不是任何方法都能作为扩展方法使用—— 它必须具有以下特征:

  • 它必须在一个非嵌套的、 非泛型的静态类中( 所以必须是一 个静态方法);
  • 它至少要有 一个参数;
  • 第一个参数必须附加 this 关键字作为前缀;
  • 第一个参数不能有其他任何修饰 符(比如out或ref);
  • 第一个参数的类型不能是指针类型。

我们来试着给Stream类写一个扩展方法:

  1. public static class StreamUtil
  2. {
  3. const int bufferSize = ;
  4. public static void CopyToo(this Stream inputStream, Stream outPutStream)
  5. {
  6. var buffer = new byte[bufferSize];
  7. int read;
  8. while ((read=inputStream.Read(buffer,,buffer.Length))>)
  9. {
  10. outPutStream.Write(buffer,,read);
  11. }
  12. }
  13.  
  14. public static byte[] ReadFull(this Stream input)
  15. {
  16. using (MemoryStream stream=new MemoryStream())
  17. {
  18. CopyToo(input,stream);
  19. return stream.ToArray();
  20. }
  21. }
  22. }

扩展方法必须在顶级的静态类中进行声明,不能是嵌套的静态类。

扩展方法假装自己是另一个类的实例方法,来看看如何使用:

  1. static void Main(string[] args)
  2. {
  3. WebRequest request = WebRequest.Create("https://www.baidu.com");
  4. using (WebResponse response = request.GetResponse())
  5. using (var resStream = response.GetResponseStream())
  6. using(var outPut=File.Open(@"C:\Users\jianxin\Desktop\test.txt",FileMode.Open))
  7. {
  8. resStream.CopyToo(outPut);
  9. }
  10. Console.WriteLine("done!");
  11. Console.ReadKey();
  12. }

之所以吧CopyTo改成CopyToo是因为Stream现在已经实现了这个扩展方法了。如果和实例方法同名,则不会去调用这个扩展方法。

一些原理

一般来说,如果你在一个对象后面调用这个对象的成员比如方法,编译器会首先从它的实例成员中去寻找,如果没有找到,他会在引入的命名空间里面去寻找合适的扩展方法。

为了决定是否使用 一个扩展方法, 编译器必须能区分扩展方法与某静态类中恰好具有合适签名的其他方法。 为此, 它会检查类和方法是否具有System.Runtime.CompilerServices.ExtensionAttribute 这个特性, 它是.NET 3. 5 新增的。 但是,编译器不检查特性来自哪个程序集。这意味着你可以在C#2或早前的版本中自己编写一个这个特性类来满足编译器的这种搜索策略。但是,谁特么还在用C#2或1呢?如果遇到多个合适的版本,还是会用“更好的选择”原则来选用最合适的那一个。

在空引用上面调用扩展方法

在空引用上面调用方法会导致NullRefrenceException的异常。但是可以调用扩展方法而不会导致异常。

  1. public static class NullUtil
  2. {
  3. public static bool IsNull(this object obj)
  4. {return obj==null;
    }
    }
  1. static void Main(string[] args)
  2. {
  3. object obj = null;
  4. Console.WriteLine(obj.IsNull());//true
  5. obj = new object();
  6. Console.WriteLine(obj.IsNull());//false
  7. Console.ReadKey();
  8. }

如果IsNull是一个实例方法那么会引发NullRefrenceException,但是扩展方法不会,可以试一试,很爽。这个写法与string.IsNullOrEmpty()形成了鲜明的对比。

Enumerable

LINQ差不多全部的功能都是用Enumerable和Queryable的扩展方法来得到的。

Enumerable中有一个不是扩展方法:Range

  1. var collection = Enumerable.Range(, );
  2. foreach (int item in collection)
  3. {
  4. Console.WriteLine(item);
  5. }

讲这个例子并不是因为它很特殊,是因为它的一个特性:延迟执行 。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扩展方法是对集合进行过滤的一种简单但又十分强大的方式,看一下代码:

  1. static void Main(string[] args)
  2. {
  3. var collection = Enumerable.Range(, )
    .Where(x=>x%!=)
    .Reverse();
  4. foreach (int item in collection)
  5. {
  6. Console.WriteLine(item);
  7. }
  8. Console.ReadKey();
  9. }

上述代码使用where过滤掉了序列中的所有偶数,然后使用Reverse对序列进行了反转。希望你此时已经注意到了一个模式—— 我们将方法调用链接到一起 了。string.Replace()就是这样的一个模式。LINQ针对数据处理进行了专门的调整,将各个单独的操作链接在一起形成了一个管道,然后让信息在这个管道中流通。

有一个效率问题:上面的代码如果先调用Reverse再调用where的话会和之前的调用顺序的效率相同么?不会,Reverse必须计算出偶数,而偶数最终是要被抛弃的。而先用where过滤掉这部分数据后,Reverse要执行的计算明显变小了。

用select方法和匿名类型进行投影

Enumerable中最重要的投影方法就是select,它操纵一个IEnumerable<TSource>,把他转化成一个IEnumerable<TResult>.它利用了延迟执行的技术,只有在每个元素被请求时才真正的执行投影。

  1. static void Main(string[] args)
  2. {
  3. var collection = Enumerable.Range(, )
  4. .Where(x => x % != )
  5. .Reverse()
  6. .Select(x => new { Original = x, SquareRoot = Math.Sqrt(x) });
  7. foreach (var item in collection)
  8. {
  9. Console.WriteLine(item);
  10. }
  11. Console.ReadKey();
  12. }

用OrderBy进行排序

在Linq中,一般是通过OrderBy或OrderByDescending。也可以继续排序。使用ThenBy和ThenByDescending。需要注意的就一点,排序不会改变原有集合,他会返回一个新的序列----LINQ操作符是无副作用 的:它们不会影响输入, 也不会改变环境。 除非你迭代的是一 个自然状态序列( 如从网络流中读取数据) 或使用含有副作用的委托参数。 这是函数式编程的方法, 可以使代码更加 可读、可测、可组合、可 预测、健壮并且线程安全。

GroupBy分组

假设要观察程序出现的bug的数量,对他们种类进行分组:

  1. bugs.GroupBy(bug => bug.AssignedTo)
  2. .Select(list => new { Developer = list.Key, Count = list.Count() })
  3. .OrderByDescending(x => x.Count);

结果是一个IGrouping<TKey, TElement>。 GroupBy有多个重载版本, 这里使用的是最简单的。 然后选择键(开发者的姓名) 和分配给他们的bug的数量。 之后,我们对结果进行排序, 最先显示分配到bug数量最多的开发者。

研究Enumerable类时, 往往会感觉搞不清楚具体发生的事情——例如,GroupBy的一个重载版本居然有4个类型参数和5个“普通”参数(3个是委托)。但是,不要惊慌——只要按照上一章描述的步骤慢慢梳理,将不同的类型赋给不同的类型参数, 直到清楚呈现出方法的样子。这样,理解起来就容易多了。 这些例子不是具体针对某个方法调用, 但我希望你能体会到将方法调用链接起来之后所发挥的巨大作用。 在这个链条中, 每个方法都获取一个原始集合, 并以某种形式返回另一个原始集合——中间可能过滤掉一些值,可能对它们进行排序,可能转换每一个元素, 可能聚合某些值, 或者做其他处理。 在许多情况下, 最终的代码都易读、易懂。在其他情况下, 它最起码也会比使用以前版本的C#写的等价代码简单得多。

使用思路和原则

流畅接口

因为扩展方法支持这种链式的调用。所以,才有了流畅接口的这个概念,比如OrderBy ThenBy等。就和自然语言一样。

C#复习笔记(4)--C#3:革新写代码的方式(扩展方法)的更多相关文章

  1. C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)

    Lambda表达式和表达式树 先放一张委托转换的进化图 看一看到lambda简化了委托的使用. lambda可以隐式的转换成委托或者表达式树.转换成委托的话如下面的代码: Func<string ...

  2. C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))

    查询表达式和LINQ to object(下) 接下来我们要研究的大部分都会涉及到透明标识符 let子句和透明标识符 let子句不过是引入了一个新的范围变量.他的值是基于其他范围变量的.let 标识符 ...

  3. C#复习笔记(4)--C#3:革新写代码的方式(用智能的编译器来防错)

    用智能的编译器来防错 本章的主要内容: 自动实现的属性:编写由字段直接支持的简单属性, 不再显得臃肿不堪: 隐式类型的局部变量:根据初始值推断类型,简化局部变量的声明: 对象和集合初始化程序:用一个表 ...

  4. Java 10 的 10 个新特性,将彻底改变你写代码的方式!

    Java 9才发布几个月,很多玩意都没整明白,现在Java 10又快要来了.. 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK ...

  5. Java 10的10个新特性,将彻底改变你写代码的方式!

    Java 9才发布几个月,很多玩意都没整明白,现在Java 10又快要来了.. 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK ...

  6. Java 8 到 Java 14,改变了哪些你写代码的方式?

    前几天,JDK 14 正式发布了,这次发布的新版本一共包含了16个新的特性. 其实,从Java8 到 Java14 ,真正的改变了程序员写代码的方式的特性并不多,我们这篇文章就来看一下都有哪些. La ...

  7. 树的直径,LCA复习笔记

    前言 复习笔记第6篇. 求直径的两种方法 树形DP: dfs(y); ans=max( ans,d[x]+d[y]+w[i] ); d[x]=max( d[x],d[y]+w[i] ); int di ...

  8. Java基础复习笔记系列 八 多线程编程

    Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...

  9. Java二次复习笔记(1)

    Java二次复习笔记(1) Java采用的默认字符编码集是Unicode. byte=byte+byte报错,值为int,而byte+=byte不报错.同样short = short+short报错, ...

随机推荐

  1. Lingo求解线性规划案例1——生产计划问题

    凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 说明: Lingo版本:                            某工厂明年根据合同,每个季度末 ...

  2. IOS解析XML文件

    这里使用NSXMLParser来解析,这个是apple自带的xml解析库,有个參考文章:http://www.raywenderlich.com/553/xml-tutorial-for-ios-ho ...

  3. SpringBoot实现JWT保护前后端分离RESTful API

    通常情况下, 将api直接暴露出来是非常危险的. 每一个api呼叫, 用户都应该附上额外的信息, 以供我们认证和授权. 而JWT是一种既能满足这样需求, 而又简单安全便捷的方法. 前端login获取J ...

  4. [HEOI2016/TJOI2016]排序

    嘟嘟嘟 首先这题的暴力是十分好写的,而且据说能得不少分. 正解写起来不难,就是不太好想. 根据做题经验,我想到了给这个序列转化成01序列,但是接下来我就不会了.还是看了题解. 因为查询只有一个数,所以 ...

  5. ubantu服务器配置ss

    阿里云 ubantu16.0(自带pip) 服务端 $ apt-get install python-pip $ pip install shadowsocks $ vim /etc/shadowso ...

  6. PHP 2 语句 数据类型 字符串函数 常量

    在 PHP 中,有两种基本的输出方法:echo 和 print. 在本教程中,我们几乎在每个例子中都会用到 echo 和 print.因此,本节为您讲解更多关于这两条输出语句的知识. PHP echo ...

  7. springmvc中messageConverter用法

    解决StringHttpMessageConverter乱码问题问题: 当我们将字符串对象通过springmvc传回浏览器时,因为StringHttpMessageConverter消息转换器中默认的 ...

  8. APP耗电量测试

    现象 APP耗电,导致电池续航能力不佳,如下图,在小米MIX2和iPhone X机型上后台静默一小时各应用的耗电排行: 基本概念 相对于PC来说,移动设备的电池电量是非常有限的,保持持久的续航能力尤为 ...

  9. Golang 学习资料

    资料 1.How to Write Go Code https://golang.org/doc/code.html 2.A Tour of Go https://tour.golang.org/li ...

  10. jupyter运行py文件(py文件和ipynb文件互转)

    有趣的事,Python永远不会缺席! 如需转发,请注明出处:小婷儿的python  https://i.cnblogs.com/EditPosts.aspx?postid=10750405 一.jup ...