前言
一直以来尤其像C#一些常见的语法,本人更愿意去探讨其内部实现的原理,为什么要这么做呢?只是为了当我真正在开发中运用语法的时候不会因为犯常识性错误或者说因为一些注意事项未曾注意到而耽误一些无谓的时间,同时也能理解的更深入而不是仅仅停留在表面(或许理解也不是太透)。(当然本人能力有限,太高深的东西必定是研究不明白了,也只有这能力了)。
概念
扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。扩展方法是在C#3.0中添加的特性。
引入
听起来有点迷糊,也许你无意之间就在用扩展方法,请看下图:
上图中有虚线下角标的方法就是扩展方法,因为其实现了IENumerable<T>接口,所以会有这些方法。那到底我们手动怎么实现呢?
实现
添加一个Person类,代码如下:
public class Person { public string Name { get; set; } public int Age { get; set; } public bool Gender { get; set; } }
现在假设如下场景:如果该项目中关于Person类字段和方法都已确定,但是在该项目完成之前boss发话要给Person添加一个需求,添加一个Preson类共有的 Hobby 方法。由于项目架构都已经确定,所以为了在不修改源代码的情况下,我们用 扩展方法 来实现。我们新添加一个扩展类 PersonExtension ,添加Hobby方法,如下:
public static class PersonExtension { public static void Hobby(this Person p) { Console.WriteLine("我们都有爱好"); } }
此时我们在控制台实例化对象试试进行访问该扩展方法Hobby,结论当然是能:
结果打印出 我们都有爱好 !至此我们就完成了扩展方法的基本使用,感觉是不是so easy!但是我就有如下几个疑问:
(1)怎么知道我们编写的是扩展方法的呢?
(2)编写的扩展方法编译器到底是怎么找到的呢?
(3)是实例方法先执行还是扩展方法先执行呢?
(4)这个 this关键字到底有什么作用呢?
(5)编写扩展方法时我们又应该注意些什么呢?
请看下文,下面我们一一进行探讨并解决。
实现原理
说到看其原理,依然是借助高大上的反编译工具了。我们看 p.Hobby(); 这段代码对应的IL代码是什么就够了,请看其对应的IL代码:
最终调用的依然是调用 PersonExtension.Hobby() ,说明 扩展方法其本质是静态方法 看来并没有什么特别之处。在Pserson中时常看见这样的构造函数
public Person(string name, int age) { this.Name = name; this.Age = age; }
所以从中我们也可以得知,谁实例化了Person谁就指代this,那同样可以理解扩展方法中的this,就是p实例化了Person那这个this就是指向P了,有时候想想语言有时候确实是相通的,此时我想到在JavaScript中不是有个Call方法吗,如果不清楚的话,我拷贝了正在学习中的JavaScript代码,如下:
function person(age,name){ this.age=age; this.name=name; } var obj=new Object(); person.call(obj,12,'小黑'); console.log(obj.age); console.log(obj.name); /* 创建空对象obj,此时调用person类的call()方法,并将obj传递进去,此时obj成为其调用上下文,此时this即obj,最终能打印出12和小黑 */
不难看出实例化的对象obj通过call方法,就指向了person中this!我们回到 p.Hobby(); 等价于 PersonExtension.Hobby(p) ,就是将你实例化的对象传到了扩展方法中的this中。那么问题来了,既然扩展方法也是方法,如果我们在Person类中定义一个方法那先是访问你定义的实例方法还是先访问你定义的扩展方法呢?这个疑问先遗留在这里,我们继续往下看,我们刚才看到 p.Hobby()实际上是调用的PersonExtension.Hobby(); 那我们接下来看看 PersonExtension.Hobby() 这个方法里面到底有什么呢?其IL代码如下:
我们注意到这个里面有个 ExtensionAttribute 标记,就字面意思就叫做【扩展特性】吧,那我们猜想一下,是不是是不是通过这个【扩展特性】来实现的扩展方法了,我们反过来看,显然我们实现了扩展方法,里面肯定有这个特性所在的程序集,要是我们可以将其特性所在的程序集进行移除,生成错误的话,是不是就说明我们的论断正确呢?试试即可,将会报错如下:
无法定义新的扩展方法,因为找不到编译器所需的类型“System.Runtime.CompilerServices.ExtensionAttribute。”是否缺少引用?
所以通过上述我们知道了通过添加 System.Runtime.CompilerServices 程序集里的 ExtensionAttribute 来实现扩展方法。此时不仅心生疑问,用它来实现扩展方法,那么反问一句,它怎么知道它是扩展方法的呢?此时只剩this关键字了,于是我去掉扩展方法中的 this 关键字重新生成试试,结果如下:
这就说明通过 this 关键字来识别为扩展方法,同时此时运行时编译器无法识别此语法,所以编译器则将通过上述IL代码中的特性来转换为运行时识别的语法。
接下来问题又来了 ,那它是先执行实例方法还是先执行扩展方法呢?我们接下来,在该类中添加同名的实例方法Hobby(),如下:
public void Hobby() { Console.WriteLine("这是实例中的我们都有爱好"); }
此时我们再调用此方法,结果在控制台打印如下:
说明 实例方法优先于扩展方法执行并且可以有同名的实例方法和扩展方法 !那么问题又来了,它是怎么找到扩展方法的呢?我们知道当调用此p.Hobby()方法时,首先肯定会去p对象去找Hobby()方法,如果有,则调用它的实例方法,如果未找到此时再去找扩展方法,问题是我们只知道通过 this 来标识为扩展方法,这个时候就得看我们的约定了,静态方法所在的类必须是静态类,我们可以想象,如果是普通类的话是不是得一个一个的找,这样岂不是很消耗性能,这是微软大大想要看到的吗?当然不是,要是为静态类,只需要到静态类去找再去静态方法去找标识为this关键字的方法,这时找到了就说明这个方法是扩展方法。
注意事项
(1)
我们再定义一个Bob类,继承该Person类,并在控制台尝试调用该扩展方法,代码如下
public class Bob : Person { } Bob b = new Bob(); b.Hobby();
结果是可以访问的,说明: 扩展方法能够被继承
(2)
当我们将扩展方法的访问修饰符修改成private会出错,如下:
说明 其访问修饰符必须是public
(3)
当我们在控制台尝试传入空引用对象调用该扩展方法时,扩展方法和控制台调用代码如下,看看结果如何:
public static class PersonExtension { public static void Hobby(this Person p) { Console.WriteLine("我们都有爱好"); } } Person p = null; p.Hobby();
此时正常调用,说明空引用可以调用扩展方法,但是将扩展方法修改如下,则会出错:未将对象设置到对象的实例。
public static class PersonExtension { public static void Hobby(this Person p) { Console.WriteLine(p.Name); } }
说明 允许空引用调用扩展方法,若扩展方法中使用了传入的实例成员,则会出现异常
总结
声明扩展方法的必须条件
方法必须定义在顶级的静态类中,并且该静态类必须直接处在命名空间下而且不能为泛型类(即方法必须放在非嵌套、非泛型的静态类中)
方法必须是静态的并且第一个参数用this关键字修饰,这个参数被叫做【参数实例】
方法的访问修饰符必须是public
至少有一个参数
不能有其他参数修饰第一个参数(如ref,out等等)
第一个参数的类型不能是指针类型
注意事项
实例方法优先于扩展方法执行(允许有同名的实例方法和扩展方法)
在空引用上可以调用扩展方法
扩展方法能够被继承
扩展方法相关原理
其本质是静态方法
this关键字的作用:(1)指向当前扩展方法中第一个参数类型的实例(2)作为标记,标记此方法为扩展方法
查找扩展方法:如果对象的实例中有该实例方法,调用它的实例方法,如果未找到此时再去找扩展方法,问题是我们只知道通过 this 来标识为扩展方法,这个时候就得看我们的约定了,只需要到静态类去找再去静态方法去找标识为this关键字的方法,这时找到了就说明这个方法是扩展方法。