方法组转换和匿名方法
前面的文章介绍过,C# 1.0中出现委托这个核心概念,在C# 2.0中,委托得到了很大的改进。C# 2.0中委托的改进为C# 3.0中的新特性提供了铺垫,当我们了解了匿名方法后,Lambda的学习就会变得相对容易。
下面就看看C# 2.0中委托的改进。
方法组转换
在C# 1.0中,如果要创建一个委托实例,就必须同时指定委托类型和符合委托签名的方法。但是,在C# 2.0中,支持了方法组转换,也就是说我们可以从方法组到一个兼容委托类型的隐式转换。所谓"方法组"(method group),其实就是一个方法名。
看一个例子:
class Program { public delegate void ReverseStringHandler(string str); private static void ReverseString(string str) { char[] charArray = str.ToCharArray(); Array.Reverse(charArray); Console.WriteLine(charArray); } static void Main(string[] args) { //C# 1.0中创建委托实例 ReverseStringHandler reverseStringHandler = new ReverseStringHandler(ReverseString); reverseStringHandler("Hello World"); //C# 2.0中通过方法组转换创建委托实例 reverseStringHandler = ReverseString; reverseStringHandler("Good morning"); } }
通过方法组转换,繁琐的委托实例创建得到了简化。
匿名方法
根据C# 1.0中了解到的知识,当我们创建一个委托实例的时候,我们需要找到一个跟委托类型签名一致的方法,用这个方法来实例化一个委托对象。
看看前面的字符串反转的例子,可能"ReverseString"这个方法在程序中只会使用一次,但是为了创建委托实例,这个方法必须要存在。
在C# 2.0中引入了匿名方法,所以有了更简单的方式实现上面的例子:
public delegate void ReverseStringHandler(string str); static void Main(string[] args) { ReverseStringHandler reverseStringHandler = delegate(string str) { char[] charArray = str.ToCharArray(); Array.Reverse(charArray); Console.WriteLine(charArray); }; reverseStringHandler("Hello World"); Console.Read(); }
从上面可以看到,匿名方法就是通过delegate关键字以及参数列表和具体语句块的实现。
所以说,如果用来实例化委托的方法比较简单,并且这个方法在其他地方使用的频率很低时,这时候就可以考虑用匿名方法来进行简化。
匿名方法工作原理
如果你对匿名方法仍有疑惑,建议你看看上面例子的IL代码。
在Main函数的IL代码中,可以看到编译器为我们生成了一个名为"<Main>b__0"的方法,方法接受一个string类型的参数。
.method private hidebysig static void Main ( string[] args ) cil managed { …… IL_0009: ldftn void AnonymousMethod.Program::'<Main>b__0'(string) …… } // end of method Program::Main
当我们查看"<Main>b__0"方法的IL代码后,可以看到这个方法就是我们在匿名方法的语句块中定义的操作。
.method private hidebysig static void '<Main>b__0' ( string str ) cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2050 // Code size 23 (0x17) .maxstack 1 .locals init ( [0] char[] charArray ) IL_0000: nop IL_0001: ldarg.0 IL_0002: callvirt instance char[] [mscorlib]System.String::ToCharArray() IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: call void [mscorlib]System.Array::Reverse(class [mscorlib]System.Array) IL_000e: nop IL_000f: ldloc.0 IL_0010: call void [mscorlib]System.Console::WriteLine(char[]) IL_0015: nop IL_0016: ret } // end of method Program::'<Main>b__0'
匿名方法的实现原理就是:编译器将在匿名方法所在的类,为每个匿名方法都创建了一个方法。编译器创建的这些方法只在IL代码中有效,在C#代码中是无效的,所以C#代码不能直接使用这些方法。
其实,匿名方法更常用的地方是把匿名方法当作一个参数传递给另一个方法。大家肯定都知道List有一个FindAll的方法来查找符合条件的item,这里FindAll的参数就是一个过滤条件的委托。
List<int> numList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach(int num in numList.FindAll(delegate(int n){return n>6;})) { Console.WriteLine(num); }
说到了这里,我们就看看系统帮我们定义的委托,在C#中,Action、Func和Predicate是系统定义的委托,我们可以直接使用。上面的FindAll的参数就是一个Predicate<T>的泛型委托。
Action委托
Action<T>是无返回值的泛型委托,Action委托可以支持至少0个参数,至多16个参数。
例如:
- Action 表示无参,无返回值的委托
- Action<int,string> 表示有传入参数int,string无返回值的委托
- Action<int,string,bool> 表示有传入参数int,string,bool无返回值的委托
对于前面的字符串反转的例子,我们可以使用Action委托进一步简化,这样我们连"ReverseStringHandler"这个委托也省略了:
static void Main(string[] args) { Action<string> reverseString = delegate(string str) { char[] charArray = str.ToCharArray(); Array.Reverse(charArray); Console.WriteLine(charArray); }; reverseString("Hello World"); Console.Read(); }
Func委托
前面看到Action委托是没有返回值的,为了解决我们有时可能需要返回值的问题,系统中又出现了Func委托。
Func<TResult>是有返回值的泛型委托,其中TResult就代表返回值的类型()。Func委托可以支持至少0个参数,至多16个参数(Func<T1,T2,…,T16,TResult>)。
例如:
- Func<int> 表示无参,返回值为int的委托
- Func<object,string,int> 表示传入参数为object, string 返回值为int的委托
- Func<object,string,int> 表示传入参数为object, string 返回值为int的委托
看个简单的例子:
Func<int, int, int> addOperation = delegate(int numA, int numB) { return numA + numB; }; Console.WriteLine(addOperation(3,4));
Predicate委托
predicate 是返回bool型的泛型委托,常常结合集合类的查询使用;Predicate有且只有一个参数,返回值固定为bool。
Predicate原型:public delegate bool Predicate<T> (T obj)
- T: 要比较的对象的类型。
- obj: 要按照由此委托表示的方法中定义的条件进行比较的对象。
- 返回值:如果 obj 符合由此委托表示的方法中定义的条件,则为 true;否则为 false。
在前面结合查询的例子中,我们直接把匿名方法"delegate(int n){return n>6;}"传给了FindAll方法。
其实也可以写成,
Predicate<int> checkNum = delegate(int n) { return n > 6; }; foreach (int num in numList.FindAll(checkNum)) {……}
忽略委托参数
在有些情况下,我们并不需要委托参数,那么匿名方法可以进一步省略参数列表,只需要使用一个delegate关键字,加上作为方法的操作使用的代码块。
看一个简单的代码段:
Action printer = delegate { Console.WriteLine("Hello world"); }; printer();
注意,这个"参数通配"(paremeter wildcarding)的特性并不能适用所有的情况,如果匿名方法能够转换成多个委托类型,那么我们就需要给编译器提供更多的信息。
举个例子,线程的构造函数设计两个委托类型,一个有参数,一个无参数。
public delegate void ParameterizedThreadStart(object obj)
public delegate void ThreadStart()
所以,当我们通过下面的语句创建线程的时候,前两条语句没有问题,但是第三条语句会有一个错误。
因为第三条语句中的匿名方法可以转换成多个委托类型,编译器就不知道怎么处理了,所以,我们需要显示给出参数列表。
Thread t1 = new Thread(delegate() { Console.WriteLine("this is t1"); }); Thread t2 = new Thread(delegate(object o) { Console.WriteLine("this is t2"); }); Thread t3 = new Thread(delegate { Console.WriteLine("this is t3"); });
总结
本篇文章介绍了C# 2.0中委托的改进,通过方法组转换和匿名方法,可以简化程序。
同时,看到了系统定义的三个委托类型,所以有些时候我们可以不用创建自己的委托;但是要这个权衡,如果我们要经常使用一个特定类型的委托,那还是建议定义一个有意义更加明显的委托类型。
匿名方法中的变量
前面一篇文章看到了C# 2.0中通过匿名方法来简化委托,下面来看看匿名方法中的变量。
闭包和不同的变量类型
闭包的基本概念是:一个函数除了能够通过提供给它的参数与环境交互之外,还能同环境进行更大程度的互动。对于C# 2.0中出现的匿名方法的闭包表现为,匿名方法能使用在声明该匿名方法的方法内部定义的局部变量。
在进一步了解闭包之前,我们先看看下面两个术语:
外部变量(outer variable):是指其作用域(scope)包括一个匿名方法的局部变量或参数(ref和out参数除外)
被捕捉的外部变量(captured outer variable):它是在匿名方法内部使用的外部变量
结合上面的解释,来看一个被捕获的变量的例子:
private static void EnclosingMethod() { //未被捕获的外部变量 int outerVariable = 2; //被匿名方法捕获的外部变量 string capturedVariable = "captured variable"; if (DateTime.Now.Hour == 23) { //普通局部变量 int normalLocalVarialbe = 3; Console.WriteLine(normalLocalVarialbe); } Action x = delegate { //匿名方法的局部变量 string anonymousLocal = "local variable of anonymous method"; //获得被捕获的外部变量 Console.WriteLine(capturedVariable); Console.WriteLine(anonymousLocal); }; x(); }
一个变量被捕获之后,被匿名方法捕获的是这个变量,为不是创建委托实例时该变量的值。下面通过一个例子来看看这句描述。
private static void CapturedVariableTesting() { string captured = "before x is created"; Action x = delegate { Console.WriteLine(captured); captured = "changed by x"; }; captured = "changed before x is invoked"; x(); Console.WriteLine(captured); captured = "before second invocation"; x(); }
代码的输出为:
在CapturedVariableTesting这个方法中,我们始终都是在使用同一个被捕获变量captured;也就是说,在匿名方法外对被捕获变量的修改,在匿名方法内部是可见的,反之亦然。
捕捉变量的用途
闭包的出现给我们带来很多的便利,直接利用被捕获变量可以简化编程,避免专门创建一些类来存储一个委托需要处理的信息。
看一个例子,我们给定一个上限,来获取List中所有小于这个上限的数字。
private static List<int> FindAllLessThan(List<int> numList, int upperLimitation) { return numList.FindAll(delegate(int num) { return num < upperLimitation; }); }
由于闭包的出现,我们不用将upperLimitation这个变量以函数参数的形式传给匿名函数,在匿名方法中可以直接使用这个被捕获的变量。
捕获变量的工作原理
前面看到的例子都比较简单,下面我们看一个稍微复杂的例子:
static void Main(string[] args) { Action x = CreateDelegateInstance(); x(); x(); Console.Read(); } private static Action CreateDelegateInstance() { int counter = 5; Action ret = delegate { Console.WriteLine(counter); counter++; }; ret(); return ret; }
代码输出为:
为什么结果是5,6,7?变量counter在CreateDelegateInstance方法结束后为什么没有被销毁?
当我们查看这个例子的IL代码时,发现编译器为我们创建了一个类"<>c__DisplayClass1"。
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1' extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field public int32 counter // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2078 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method '<>c__DisplayClass1'::.ctor .method public hidebysig instance void '<CreateDelegateInstance>b__0' () cil managed { // Method begins at RVA 0x2080 // Code size 28 (0x1c) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_0007: call void [mscorlib]System.Console::WriteLine(int32) IL_000c: nop IL_000d: ldarg.0 IL_000e: dup IL_000f: ldfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_0014: ldc.i4.1 IL_0015: add IL_0016: stfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_001b: ret } // end of method '<>c__DisplayClass1'::'<CreateDelegateInstance>b__0' } // end of class <>c__DisplayClass1
而在CreateDelegateInstance方法的IL代码中可以看到,CreateDelegateInstance的局部变量counter实际上就是"<>c__DisplayClass1"对象的counter字段。
IL_0000: newobj instance void AnonymousMethod.Program/'<>c__DisplayClass1'::.ctor() IL_0005: stloc.1 IL_0006: nop IL_0007: ldloc.1 IL_0008: ldc.i4.5 IL_0009: stfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter
通过上面的分析可以看到,编译器创建了一个额外的类来容纳变量,CreateDelegateInstance方法拥有该类的一个实例引用,并通过这个引用访问counter变量。counter这个局部变量并不是在"调用栈"空间上,这也就解释了为什么函数返回后,这个变量没有被销毁。
在上面的例子中只有一个委托实例,下面再看一个拥有多个委托实例的例子:
static void Main(string[] args) { List<Action> list = new List<Action>(); for(int index = 0; index < 5; index++) { int counter = index * 10; list.Add(delegate { Console.WriteLine(counter); counter++; }); } foreach (Action x in list) { x(); } list[0](); list[0](); list[1](); Console.Read(); }
代码输出为:
通过输出可以看到,每个委托实例将捕获不同的变量。
所以被捕获变量的声明期可以总结为:对于一个被捕获的变量,只要还有任何委托实例在引用它,它就会一直存在;当一个变量被捕获时,捕获的是变量的"实例"。
总结
本文介绍了闭包和不同的变量类型。在匿名方法中,通过被捕获变量,我们可以使用"现有"的上下文信息,而不必专门设置额外的类型来存储一些已知的数据
同时,介绍了被捕获变量的生命期,通过IL代码看到了被捕获变量的工作原理。