.NET 扩展方法 (二)
上一篇随笔 .NET 扩展方法 (一) 已经对 扩展方法有了大致的介绍,这篇算是一个补充,让我们来看一下扩展方法的几个细节:
一、扩展方法具有继承性
当使用扩展方法扩展一个类型的时候,其也扩展了派生类,所以上一篇的遗留问题“如果给object添加一个扩展方法会出现什么效果呢?” 的
答案就是——所有类型都将扩展该方法。object类已经经受住了时间的考验,我们似乎也找不到更合适的理由来扩展object类。从另外的
角度考虑,如果扩展了object类,很有可能会给“智能敏感提示”造成污染,以至于填充了过多的垃圾信息(因为许多类型也根本用不到该方法)。
二、扩展方法允许和扩展类型原有方法相等效的方法存在 (此处是个雷)
由于汉语语言表述的所带来的不易理解性,我们还是直接用代码来解释吧,如下的代码片段:
public static class StringExtentsion { public static string ToString(this string str) { return "Extentsion" + str; } } class Program { static void Main(string[] args) { string str = "test"; Console.WriteLine(str.ToString()); // 输出结果为: test,也就说编译器会优先选用原有类的实例化方法,如果没找到匹配方法再寻找扩展方法 Console.Read(); } }
由上述的代码片段可以知:StringExtentsion类中扩展方法ToString 和 String类的原有的ToString方法 对于客户端代码而言,它们的语法表象是
一样的,但本质上一个是StringExtentsion类的静态方法,一个是String类的实例化方法。然而编译运行没有产生错误,更没有产生警告。所以在
这种情况下很容“埋雷”,一不小心就会中招。有人也许会说:我注意一下不要和.NET类库的方法重名就可以了。但是你能保证 .NET 6、甚至.NET 10
的方法名和你写的绝对不重名吗?所以,扩展方法存在着版本控制的问题。
三、扩展方法允许在两个或多个类中共存相同的扩展方法
public static class StringExtentsion { public static bool IsEmpty(this string str) { return string.IsNullOrWhiteSpace(str); } } public static class OtherStringExtentsion { public static bool IsEmpty(this string str) { return string.IsNullOrWhiteSpace(str); } } class Program { static void Main(string[] args) { string str = null; //bool result = str.IsEmpty(); // 这种写法会产生编译错误:不明确的调用,因为它能找到两个扩展方法 bool result = StringExtentsion.IsEmpty(str); // 必须显示使用类调用静态方法 Console.WriteLine(result); Console.Read(); } }
四、扩展其他类型的方法
扩展方法不仅可以扩展类类型,而且可以扩展接口、枚举、委托、结构、数组及对应的泛型类型 等类型。
在这里着重说一下扩展接口,任何 “实现了接口的类型对象” 都可以调用接口上的扩展方法。针对这一特点,我们完全可以将实现接口
的共用代码放进扩展方法里,实现代码复用。从形式上来看,该特点是 “单继承 + 接口” 与 “多继承”的中间产物,已经有了“多继承”的影子。
五、委托与扩展方法
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MethodDemo { public static class StringExtentsion { public static string ShowString(this string str) { return "ShowString:" + str; } } class Program { static void Main(string[] args) { string str = "meng"; Func<string> fun = str.ShowString; fun(); Func<string> fun2 = str.ToString; fun2(); Console.Read(); } } }
如果你已经看过了 .NET 扩展方法 (一) ,也许你能猜到我接下来想说什么了吧。没错,看一下IL代码吧,看看编译器在背后搞了哪些“怪”。
顺便借助这个机会,说几个IL指令。
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint 4 // 代码大小 55 (0x37) 5 .maxstack 2 6 7 // 初始化3个局部变量 8 .locals init ([0] string str, 9 [1] class [mscorlib]System.Func`1<string> fun, 10 [2] class [mscorlib]System.Func`1<string> fun2) 11 IL_0000: nop 12 13 //将 "meng" 这个字符串对象的引用 入栈 14 IL_0001: ldstr "meng" 15 16 // 将栈顶的值赋值给第0个局部变量(即 str),栈顶值出栈 17 IL_0006: stloc.0 18 19 // 将第0个局部变量入栈 (即 str 入栈) 20 IL_0007: ldloc.0 21 22 // 将 MethodDemo.StringExtentsion类的静态方法ShowString的指针入栈 23 IL_0008: ldftn string MethodDemo.StringExtentsion::ShowString(string) 24 25 // 调用构造函数 new一个 Func<String>类型的委托 26 IL_000e: newobj instance void class [mscorlib]System.Func`1<string>::.ctor(object, 27 native int) 28 IL_0013: stloc.1 29 IL_0014: ldloc.1 30 31 // 调用fun对象的Invoke方法 32 IL_0015: callvirt instance !0 class [mscorlib]System.Func`1<string>::Invoke() 33 IL_001a: pop 34 IL_001b: ldloc.0 35 IL_001c: dup 36 37 // 将str 对象的实例化方法ToString方法的指针入栈 38 IL_001d: ldvirtftn instance string [mscorlib]System.Object::ToString() 39 IL_0023: newobj instance void class [mscorlib]System.Func`1<string>::.ctor(object, 40 native int) 41 IL_0028: stloc.2 42 IL_0029: ldloc.2 43 IL_002a: callvirt instance !0 class [mscorlib]System.Func`1<string>::Invoke() 44 IL_002f: pop 45 IL_0030: call int32 [mscorlib]System.Console::Read() 46 IL_0035: pop 47 IL_0036: ret 48 } // end of method Program::Main
核心的IL指令已经给予了差不多的注释,根据IL指令可以得出结果:我们又被编译器“欺骗”了一次,fun对象保存的方法指针是
MethodDemo.StringExtentsion类的静态方法ShowString的指针。fun2对象保存的方法的指针str对象的ToString方法的指针。