C#复习笔记(4)--C#3:革新写代码的方式(扩展方法)

扩展方法

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

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

对于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等。就和自然语言一样。

原文地址:https://www.cnblogs.com/pangjianxin/p/8672151.html

时间: 2024-10-06 13:54:42

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

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

用智能的编译器来防错 本章的主要内容: 自动实现的属性:编写由字段直接支持的简单属性, 不再显得臃肿不堪: 隐式类型的局部变量:根据初始值推断类型,简化局部变量的声明: 对象和集合初始化程序:用一个表达式就能创建和初始化对象: 隐式类型的数组:根据内容推断数组的类型,从而简化数组的创建过程: 匿名类型:允许创建新的临时类型来包含简单的属性: 自动实现的属性 这个特性简单的我都不想描述,但是为了保持内容的完整性,放一张图: 和匿名方法还有迭代器一样,它在编译器的帮助下会生成一个后备字段. 自动实现

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

Lambda表达式和表达式树 先放一张委托转换的进化图 看一看到lambda简化了委托的使用. lambda可以隐式的转换成委托或者表达式树.转换成委托的话如下面的代码: Func<string, int> getLength = s => s.Length; 转换成表达式树的话是下面的代码: Expression<Func<string, int>> getLength = s => s.Length; 委托方面的东西前面都做了详细的介绍.我们主要学习表达

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

查询表达式和LINQ to object(下) 接下来我们要研究的大部分都会涉及到透明标识符 let子句和透明标识符 let子句不过是引入了一个新的范围变量.他的值是基于其他范围变量的.let 标识符=表达式; 首先展示一个不适用let操作符来使用的按用户名称长度来排序: ... var queryWithoutLet = from user in SampleData.AllUsers orderby user.Name.Length select user; foreach (User us

自己写的一些数据类型扩展方法,还不是很完善,只是现在用到什么就写了什么而已,以后会慢慢更新

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Xml.Linq; 6 7 namespace Tools 8 { 9 /// <summary> 10 /// Dictionary扩展方法 11 /// </summary> 12 public static class DictionaryExtensions

写出优美代码的方式,两个习惯:一步到位VS迭代优化

最近把手头这个安卓APP的所有事务性方法都写完了,有了以下体会,新手体会,老鸟轻拍 想写成优美代码的人一般都会有这样的想法: 一定要在写每一句代码,写每一个方法,构造每一个类的时候,都要记得优化:解耦以复用,拆分方法以复用,使用循环减少冗余,限制循环次数减少无效操作,等等.. 这个想法一定没有错,但很多时候往往会是这样的情况: 当功能一复杂,比如你已经分解了几个方法,比如你已经使用了几层循环(有点过分...),比如在多线程中 你经常无法一步到位地完成那么多优化 这往往造成你写一句代码会思考很久

程序员如何像写代码一样找女朋友

在程序员的世界里,妹子是稀有动物,女神就更是凤毛麟角了,大部分程序员由于经常面对电脑,缺乏与人的沟通交流,加上软件行业的工作特殊性,因此找女朋友更是难上加难.那么,程序员如何用自己的方法去追求心仪的女生呢?有这个冲动的朋友请继续看下去. 1.需求分析 根据自己的性格特点.经济实力,合理定位:适合自己的女性范围,也就是软件工程里常谈到:需求分析.自己最想找什么样的女孩,譬如:身高在什么范 围,年龄在什么范围,学历在什么范围,相貌有什么要求,对性格有什么偏好,喜静还是偏活泼.你越能更多了解自己,知道

安卓开发复习笔记——Fragment+FragmentTabHost组件(实现新浪微博底部菜单)

记得之前写过2篇关于底部菜单的实现,由于使用的是过时的TabHost类,虽然一样可以实现我们想要的效果,但作为学习,还是需要来了解下这个新引入类FragmentTabHost 之前2篇文章的链接: 安卓开发复习笔记——TabHost组件(一)(实现底部菜单导航) 安卓开发复习笔记——TabHost组件(二)(实现底部菜单导航) 关于Fragment类在之前的安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)也介绍过,这里就不再重复阐述了. 国际惯例,先来张效果图: 下面

安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)

什么是ViewPager? 关于ViewPager的介绍和使用,在之前我写过一篇相关的文章<安卓开发复习笔记——ViewPager组件(仿微信引导界面)>,不清楚的朋友可以看看,这里就不再重复. 什么是Fragment? Fragment是Android3.0后新增的概念,Fragment名为碎片,不过却和Activity十分相似,具有自己的生命周期,它是用来描述一些行为或一部分用户界面在一个Activity中,我们可以合并多个Fragment在一个单独的activity中建立多个UI面板,或

[Java基础] Java线程复习笔记

先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少.线程可以认为是轻量级的进 程,充分地利用线程可以使得同一个进程中执行多种任务.Java是第一个在语言层面就支持线程操作的主流编程语言.和进程类似,线程也是各自独立的,有自 己的栈,自己的局部变量,自己的程序执行并行路径,但线程的独立性又没有进程那么强,它们共享内存,文件资源,以及其他进程层面的状态等.同一个进程内的 多个线程共享同样的内存空间,这也就意味着这些线程可以访问同样的变量和