.NET中那些所谓的新语法之二:匿名类、匿名方法与扩展方法
开篇:在上一篇中,我们了解了自动属性、隐式类型、自动初始化器等所谓的新语法,这一篇我们继续征程,看看匿名类、匿名方法以及常用的扩展方法。虽然,都是很常见的东西,但是未必我们都明白其中蕴含的奥妙。所以,跟着本篇的步伐,继续来围观。
/* 新语法索引 */
2.隐式类型 var5.匿名类 & 匿名方法6.扩展方法10.LINQ查询表达式
一、匿名类:[ C# 3.0/.NET 3.x 新增特性 ]
1.1 不好意思,我匿了
在开发中,我们有时会像下面的代码一样声明一个匿名类:可以看出,在匿名类的语法中并没有为其命名,而是直接的一个new { }就完事了。从外部看来,我们根本无法知道这个类是干神马的,也不知道它有何作用。
var annoyCla1 = new { ID = 10010, Name = "EdisonChou", Age = 25 }; Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID,annoyCla1.Name, annoyCla1.Age);
经过调试运行,我们发现匿名类完全可以实现具名类的效果:
1.2 深入匿名类背后
既然我们发现匿名类可以完全实现具名类的效果,那么我们可以大胆猜测编译器肯定在内部帮我们生成了一个类似具名类的class,于是,我们还是借助反编译工具对其进行探索。通过Reflector反编译,我们找到了编译器生成的匿名类如下图所示:
从上图可以看出:
(1)匿名类被编译后会生成一个[泛型类],可以看到上图中的<>f__AnonymousType0<<ID>j__TPar, <Name>j__TPar, <Age>j__TPar>就是一个泛型类;
(2)匿名类所生成的属性都是只读的,可以看出与其对应的字段也是只读的;
所以,如果我们在程序中为属性赋值,那么会出现错误;
(3)可以看出,匿名类还重写了基类的三个方法:Equals,GetHashCode和ToString;我们可以看看它为我们所生成的ToString方法是怎么来实现的:
实现的效果如下图所示:
1.3 匿名类的共享
可以想象一下,如果我们的代码中定义了很多匿名类,那么是不是编译器会为每一个匿名类都生成一个泛型类呢?答案是否定的,编译器考虑得很远,避免了重复地生成类型。换句话说,定义了多个匿名类的话如果符合一定条件则可以共享一个泛型类。下面,我们就来看看有哪几种情况:
(1)如果定义的匿名类与之前定义过的一模一样:属性类型和顺序都一致,那么默认共享前一个泛型类
var annoyCla1 = new { ID = 10010, Name = "EdisonChou", Age = 25 }; Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID, annoyCla1.Name, annoyCla1.Age); Console.WriteLine(annoyCla1.ToString()); // 02.属性类型和顺序与annoyCla1一致,那么共同使用一个匿名类 var annoyCla2 = new { ID = 10086, Name = "WncudChou", Age = 25 }; Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID, annoyCla1.Name, annoyCla1.Age); Console.WriteLine("Is The Same Class of 1 and 2:{0}", annoyCla1.GetType() == annoyCla2.GetType());
通过上述代码中的最后两行:我们可以判断其是否是一个类型?答案是:True
(2)如果属性名称和顺序一致,但属性类型不同,那么还是共同使用一个泛型类,只是泛型参数改变了而已,所以在运行时会生成不同的类:
var annoyCla3 = new { ID = "EdisonChou", Name = 10010, Age = 25 }; Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla3.ID, annoyCla3.Name, annoyCla3.Age); Console.WriteLine("Is The Same Class of 2 and 3:{0}", annoyCla3.GetType() == annoyCla2.GetType());
我们刚刚说到虽然共享了同一个泛型类,只是泛型参数改变了而已,所以在运行时会生成不同的类。所以,那么可以猜测到最后两行代码所显示的结果应该是False,他们虽然都使用了一个泛型类,但是在运行时生成了两个不同的类。
(3)如果数据型名称和类型相同,但顺序不同,那么编译器会重新创建一个匿名类
var annoyCla4 = new { Name = "EdisonChou", ID = 10010, Age = 25 }; Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla4.ID, annoyCla4.Name, annoyCla4.Age); Console.WriteLine("Is The Same Class of 2 and 4:{0}", annoyCla4.GetType() == annoyCla2.GetType());
运行判断结果为:False
通过Reflector,可以发现,编译器确实重新生成了一个泛型类:
二、匿名方法:[ C# 2.0/.NET 2.0 新增特性 ]
2.1 从委托的声明说起
C#中的匿名方法是在C#2.0引入的,它终结了C#2.0之前版本声明委托的唯一方法是使用命名方法的时代。不过,这里我们还是看一下在没有匿名方法之前,我们是如何声明委托的。
(1)首先定义一个委托类型:
public delegate void DelegateTest(string testName);
(2)编写一个符合委托规定的命名方法:
public void TestFunc(string name) { Console.WriteLine("Hello,{0}", name); }
(3)最后声明一个委托实例:
DelegateTest dgTest = new DelegateTest(TestFunc); dgTest("Edison Chou");
(4)调试运行可以得到以下输出:
由上面的步凑可以看出,我们要声明一个委托实例要为其编写一个符合规定的命名方法。但是,如果程序中这个方法只被这个委托使用的话,总会感觉代码结构有点浪费。于是,微软引入了匿名方法,使用匿名方法声明委托,就会使代码结构变得简洁,也会省去实例化的一些开销。
2.2 引入匿名方法
(1)首先,我们来看看上面的例子如何使用匿名方法来实现:
DelegateTest dgTest2 = new DelegateTest(delegate(string name) { Console.WriteLine("Good,{0}", name); });
从运行结果图中可以看出,原本需要传递方法名的地方我们直接传递了一个方法,这个方法以delegate(参数){方法体}的格式编写,在{}里边直接写了方法体内容。于是,我们不禁欢呼雀跃,又可以简化一些工作量咯!
(2)其次,我们将生成的程序通过Reflector反编译看看匿名方法是怎么帮我们实现命名方法的效果的。
①我们可以看到,在编译生成的类中,除了我们自己定义的方法外,还多了两个莫名其妙的成员:
②经过一一查看,原来编译器帮我们生成了一个私有的委托对象以及一个私有的静态方法。我们可以大胆猜测:原来匿名方法不是没有名字的方法,还是生成了一个有名字的方法,只不过这个方法的名字被藏匿起来了,而且方法名是编译器生成的。
③经过上面的分析,我们还是不甚了解,到底匿名方法委托对象在程序中是怎么体现的?这里,我们需要查看Main方法,但是通过C#代码我们没有发现一点可以帮助我们理解的。这时,我们想要刨根究底就有点麻烦了。还好,在高人指点下,我们知道可以借助IL(中间代码)来分析一下。于是,在Reflector中切换展示语言,将C#改为IL,就会看到另外一番天地。
(3)由上面的分析,我们可以做出结论:编译器对于匿名方法帮我们做了两件事,一是生成了一个私有静态的委托对象和一个私有静态方法;二是将生成的方法的地址存入了委托,在运行时调用委托对象的Invoke方法执行该委托对象所持有的方法。因此,我们也可以看出,匿名方法需要结合委托使用。
2.3 匿名方法扩展
(1)匿名方法语法糖—更加简化你的代码
在开发中,我们往往会采用语法糖来写匿名方法,例如下面所示:
DelegateTest dgTest3 = delegate(string name) { Console.WriteLine("Goodbye,{0}", name); }; dgTest3("Edison Chou");
可以看出,使用该语法糖,将new DelegateTest()也去掉了。可见,编译器让我们越来越轻松了。
(2)传参也有大学问—向方法中传入匿名方法作为参数
①在开发中,我们往往声明了一个方法,其参数是一个委托对象,可以接受任何符合委托定义的方法。
static void InvokeMethod(DelegateTest dg) { dg("Edison Chou"); }
②我们可以将已经定义的方法地址作为参数传入InvokeMethod方法,例如:InvokeMethod(TestFunc); 当然,我们也可以使用匿名方法,不需要单独定义就可以调用InvokeMethod方法。
InvokeMethod(delegate(string name) { Console.WriteLine("Fuck,{0}", name); });
(3)省略省略再省略—省略"大括号"
经过编译器的不断优化,我们发现连delegate后边的()都可以省略了,我们可以看看下面一段代码:
InvokeMethod(delegate { Console.WriteLine("I love C sharp!"); });
而我们之前的定义是这样的:
public delegate void DelegateTest(string testName); static void InvokeMethod(DelegateTest dg) { dg("Edison Chou"); }
我们发现定义时方法是需要传递一个string类型的参数的,但是我们省略了deletegate后面的括号之后就没有参数了,那么结果又是什么呢?经过调试,发现结果输出的是:I love C sharp!
这时,我们就有点百思不得其解了!明明都没有定义参数,为何还是满足了符合委托定义的参数条件呢?于是,我们带着问题还是借助Reflector去一探究竟。
①在Main函数中,可以看到编译器为我们自动加上了符合DelegateTest这个委托定义的方法参数,即一个string类型的字符串。虽然,输出的是I love C sharp,但它确实是符合方法定义的,因为它会接受一个string类型的参数,尽管在方法体中没有使用到这个参数。
②刚刚在Main函数中看到了匿名方法,现在可以看看编译器为我们所生成的命名方法。
三、扩展方法:[ C# 3.0/.NET 3.x 新增特性 ]
3.1 神奇—初玩扩展方法
(1)提到扩展方法,我想大部分的园友都不陌生了。不过还是来看看MSDN的定义:
MSDN 说:扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。这里的“添加”之所以使用引号,是因为并没有真正地向指定类型添加方法。
那么,有时候我们会问:为什么要有扩展方法呢?这里,我们可以顾名思义地想一下,扩展扩展,那么肯定是涉及到可扩展性。在抽象工厂模式中,我们可以通过新增一个工厂类,而不需要更改源代码就可以切换到新的工厂。这里也是如此,在不修改源码的情况下,为某个类增加新的方法,也就实现了类的扩展。
(2)空说无凭,我们来看看在C#中是怎么来判断扩展方法的:通过智能提示,我们发现有一些方法带了一个指向下方的箭头,查看“温馨提示”,我们知道他是一个扩展方法。所得是乃,原来我们一直对集合进行筛选的Where()方法居然是扩展方法而不是原生的。
我们再来看看使用Where这个扩展方法的代码示例:
static void UseExtensionMethod() { List<Person> personList = new List<Person>() { new Person(){ID=1,Name="Big Yellow",Age=10}, new Person(){ID=2,Name="Little White",Age=15}, new Person(){ID=3,Name="Middle Blue",Age=7} }; // 下面就使用了IEnumerable的扩展方法:Where var datas = personList.Where(delegate(Person p) { return p.Age >= 10; }); foreach (var data in datas) { Console.WriteLine("{0}-{1}-{2}", data.ID, data.Name, data.Age); } }
上述代码使用了Where扩展方法,找出集合中Age>=10的数据形成新的数据集并输出:
(3)既然扩展方法是为了对类进行扩展,那么我们可不可以进行自定义扩展呢?答案是必须可以。我们先来看看扩展方法是如何的定义的,可以通过刚刚的IEnumerable接口中的Where方法定义来看看有哪些规则:通过 转到定义 的方式,我们可以看到在System.Linq命名空间下,有叫做Enumerable的这样一个静态类,它的成员方法全是静态方法,而且每个方法的大部分第一参数都是以this开头。于是,我们可以总结出,扩展方法的三个要素是:静态类、静态方法以及this关键字。
public static class Enumerable { public static IEnumerable<TSource> Union<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer); }
那么问题又来了:为何一定得是static静态的呢?这个我们都知道静态方法是不属于某个类的实例的,也就是说我们不需要实例化这个类,就可以访问这个静态方法。所以,你懂的啦。
(4)看完扩展方法三要素,我们就来自动动手写一个扩展方法:
public static class PersonExtension { public static string FormatOutput(this Person p) { return string.Format("ID:{0},Name:{1},Age:{2}", p.ID, p.Name, p.Age); } }
上面这个扩展方法完成了一个格式化输出Person对象属性信息的字符串构造,可以完成上面例子中的输出效果。于是,我们可以将上面的代码改为以下的方式进行输出:
static void UseMyExtensionMethod() { List<Person> personList = new List<Person>() { new Person(){ID=1,Name="Big Yellow",Age=10}, new Person(){ID=2,Name="Little White",Age=15}, new Person(){ID=3,Name="Middle Blue",Age=7} }; var datas = personList.Where(delegate(Person p) { return p.Age >= 10; }); foreach (var data in datas) { Console.WriteLine(data.FormatOutput()); } }
3.2 嗦嘎—探秘扩展方法
刚刚我们体验了扩展方法的神奇之处,现在我们本着刨根究底的学习态度,借助Reflector看看编译器到底帮我们做了什么工作?
(1)通过反编译刚刚那个UseMyExtensionMethod方法,我们发现并没有什么奇怪之处。
(2)这时,我们可以将C#切换到IL代码看看,或许会有另一番收获?于是,果断切换之后,发现了真谛!
原来编译器在编译时自动将Person.FormatOutput更改为了PersonExtension.FormatOutput,这时我们仿佛茅塞顿开,所谓的扩展方法,原来就是静态方法的调用而已,所德是乃(原来如此)!于是,我们可以将这样认为:person.FormatOutput() 等同于调用 PersonExtension.FormatOutput(person);
(3)再查看所编译生成的方法,发现this关键已经消失了。我们不禁一声感叹,原来this只是一个标记而已,标记它是扩展的是哪一个类型,在方法体中可以对这个类型的实例进行操作。
3.3 注意—总结扩展方法
(1)如何定义扩展方法:
定义静态类,并添加public的静态方法,第一个参数 代表 扩展方法的扩展类。
a) 它必须放在一个非嵌套、非泛型的静态类中(的静态方法);
b) 它至少有一个参数;
c) 第一个参数必须附加 this 关键字;
d) 第一个参数不能有任何其他修饰符(out/ref)
e) 第一个参数不能是指针类型
(2)当我们把扩展方法定义到其它程序集中时,一定要注意调用扩展方法的环境中需要包含扩展方法所在的命名空间!
(3)如果要扩展的类中本来就有和扩展方法的名称一样的方法,到底会调用成员方法还是扩展方法呢?
答案:编译器默认认为一个表达式是要使用一个实例方法,但如果没有找到,就会检查导入的命名空间和当前命名空间里所有的扩展方法,并匹配到适合的方法。
参考文章
(1)一线码农,《来看看两种好玩的方法:扩展方法和分部方法》:http://www.cnblogs.com/huangxincheng/p/4021192.html
(2)Anders Cui,《扩展方法浅谈》:http://www.cnblogs.com/anderslly/archive/2010/01/18/using-extension-methods.html
附件下载
NewGrammerDemos v1.1 : http://pan.baidu.com/s/1pJsOrvd