CLR via C# 委托 C#为委托提供的简化语法

许多程序员因为语法奇怪而对委托有抗拒感。例如:

其中button_click是方法,像这样:

第一行代码的思路是向按钮控件登记button_click方法的地址,以便在按钮被单击时调用方法。许多程序员认为,仅仅为了指定button_click方法的地址,就构造一个EventHander委托对象,这显得有点不可思议。然而,构造EventHander委托对象是CLR要求的,因为这个对象提供了一个包装器,可确保(被包装的)方法只能以类型安全的方式调用。这个包装器还支持调用实例方法和委托链。遗憾的是,很多程序员并不像仔细研究这些细节。

程序员更喜欢想下面这样写代码:

幸好,Microsoft C#编译器确实为程序员提供了用于委托的一些简化语法。接下来的描述基本上只是C#的语法糖。这些简化语法为程序员提供了一种更简单的方式生成CLR和其他编程语言处理委托时所必需的IL代码。这些简化语法是C#特有的,其他编译器可能还没有提供额外的委托简化语法。

//--简化语法1:不需要构造委托对象

如前所述,C#允许指定回调方法的名称,不必构造委托对象包装器。例如:

ThreadPool类的静态QueueUserWorkItem方法期待一个WaitCallback委托对象,

委托对象中包装的是对SomeAsyncTask方法的引用。由于C#编译器能自己进行推断,所以可以省略构造WaitCallback委托对象的代码,使代码的可读性更佳,也更容易理解。当然,当代码编译时,C#编译器还是会生成IL代码来新建WaitCallback委托对象---只是语法得到了简化而已。

//--简化语法2:不需要定义回调方法(lambda表达式)

C#允许以内联(直接嵌入)的方式写回调方法的代码,不必在它自己的方法中写。例如,前面的代码可以这样重写:

注意,传给QueueUserWorkItem方法的第一个参数是代码!更正式地说,这是一个C# lambda 表达式,可通过C# lambda 表达式操作符 => 来轻松识别。lambda 表达式可在编译器预计会看到一个委托的地方使用。编译器看到这个lambda 表达式之后,会在类(本例是AClass)中自动定义一个新的私有方法。这个新方法称为 匿名函数,因为方法名称由编译器自动创建,而且你一般不知道这个名称。利用ILDasm.exe检查编译器生成的代码:

图中蓝色部分就是该方法的命名。

编译器选择的方法名以<符号开头,这是因为在C#中,标识符是不能包含<符号的;这就确保了你不会碰巧定义了一个编译器自动选择的名称。顺便说一句,虽然C#禁止标识符包含<符号,但是CLR允许,这是为什么不会出错的原因。还要注意,虽然可将方法名作为字符串来传递,通过反射来访问方法,但C#语言规范指出,编译器生成的名称的方式是没有任何保证的。例如,每次编译代码,编译器都可能为方法生成一个不同的名称。

注意,C#编译器向方法应用了System.Runtime.CompilerServices.CompilerGeneratedAttribute特性,指出该方法由编译器生成,而非程序员写的。=>操作符右侧的代码被放入编译器生成的方法中。

注意,写lambda表达式时没有办法向编译器生成的方法引用定制特性。此外,不能向方法应用任何方法修饰符(比如unsafe)。但这一般不会有什么问题,因为编译器生成的匿名函数总是私有方法,而且方法要么是静态的,要么是非静态的,具体取决于方法是否访问了任何实例成员。所以,没必要向方法应用public,protected,internal,virtual,sealed,override或abstract之类的修饰符。

lambda表达式必须匹配WaitCallback委托:获取一个object并返回void。但在指定参数名称时,我简单地将obj放在=>操作符的左侧。在=>操作符右侧,Console.WriteLine碰巧本来就返回void。然而,如果在这里放一个返回值不为void的表达式,编译器生成的代码会直接忽略返回值,因为编译器生成的方法必须用void返回类型类满足WaitCallback委托。

另外还要注意,匿名函数被标记为private,禁止非类型内定义的代码访问(尽管反射能揭示出方法确实存在)。另外,匿名函数被标记为static,因为代码没有访问任何实例成员(也不能访问,因为CallbackWithoutNewingASelegateObject本身是静态方法)。不过,代码可用引用类中定义的任何静态字段或静态方法。

例如:

如果 CallbackWithoutNewingASelegateObject 方法不是静态的,匿名函数的代码就可以包含对实例成员的引用。不包含实例成员引用,编译器仍会生成匿名函数,因为它的效率比实例方法高。之所以高效,是因为不需要额外的this参数。但是,如果匿名函数的代码确实引用了实例成员,编译器就会生成非静态匿名函数。

=>操作符左侧供指定传给lambda表达式的参数名称。下面总结了一些规则:

=> 操作符右侧供指定匿名函数主体。通常,主体包含要么简单、要么复杂的表达式,并最终返回非void值。上述代码为Func委托变量赋值都是返回string的lambda表达式。匿名函数主体经常只由一个语句构成。

如果主体由两个或更多语句构成,必须用大括号将语句封闭。在用了大括号的情况下,如果委托期待返回值,还必须在主体中添加return语句。

重要提示:lambda表达式的主要优势在于,它从你的源代码中移除一个“间接层”(a level of indirection),或者说避免了迂回。正常情况下必须写一个单独的方法,命名该方法,再需要委托的地方传递这个方法名。方法名提供了引用代码主体的一种方式,如果要在多个地方引用同一个代码主体,单独写一个方法并命名确实是理想方案。但如果只需在代码中引用这个主体一次,那么lambda表达式允许直接内联哪些代码,不必为它分配名称,从而提供了编程效率。

//--简化语法3:局部变量不需要手动包装到类中即可传给回调方法

有时希望回调代码引用存在于定义方法中的局部参数或变量。

 

 

这个例子生动地演示了C#如何简单地实现一个非常复杂的任务。方法定义了一个参数numToDo和两个局部变量squares和done。而且lambda表达式的主体引用了这些变量。

现在,想象lambda表达式主体的代码在一个单独的方法中(确实如此,这是CLR要求的)。变量的值如何传给这个单独的方法?唯一的办法是定义一个新的辅助类,这个类要为打算传给回调代码的每个值都定义一个字段。此外回调代码还必须定义成辅助类中的实例方法。然后,UsingLocalVariablesInTheCallbackCode方法必须构造辅助类的实例,用方法定义的局部变量的值来初始化该实例中的字段。然后,构造绑定到辅助对象/实例方法的委托对象。

注意:当lambda表达式造成编译器生成一个类,而且参数/局部变量被转成该类的字段后,变量引用的对象的生存期被延长了。正常情况下,在方法中最后一次使用参数/局部变量之后,这个参数/局部变量就会被“离开作用域”结束其生命期。但是,将变量转变成字段后,只要包含字段的那个对象不“死”,字段引用的对象也不会“死”。这在大多数引用程序中不是大问题,但有时要注意一下。

这项工作给常单调乏味,而且容易出错。但理所当然地,热门它们全部由C#自动完成。

重要提示:

C#的lambda表达式功能很容易被滥用。我们需要时间熟悉它。毕竟,你在方法中写的代码实际不在这个方法中。除了有违直觉,还使调试和单步执行变得比较有挑战性。

可以定个规则:如果需要在回调方法包含3行以上的代码,就不使用lambda表达式。

使用得当,匿名方法确实会显著提高开发人员的效率和代码的可维护性。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值