C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质
前言
C# 3.0 引入了 Lambda 表达式,程序员们很快就开始习惯并爱上这种简洁并极具表达力的函数式编程特性。
本着知其然,还要知其所以然的学习态度,笔者不禁想到了几个问题。
(1)匿名函数(匿名方法和Lambda 表达式统称)如何实现的?
(2)Lambda表达式除了书写格式之外还有什么特别的地方呢?
(3)匿名函数是如何捕获变量的?
(4)神奇的闭包是如何实现的?
本文将基于CIL代码探寻Lambda表达式和匿名方法的本质。
笔者一直认为委托可以说是C#最重要的元素之一,有很多东西都是基于委托实现的,如事件。关于委托的详细说明已经有很多好的资料,本文就不再墨迹,有兴趣的朋友可以去MSDN看看http://msdn.microsoft.com/zh-cn/library/900fyy8e(v=VS.80).aspx
目录
三种实现委托的方法
从CIL代码比较匿名方法和Lambda表达式区别
从CIL代码研究带有参数的委托
从CIL代码研究匿名函数捕获变量和闭包的实质
正文
1.三种实现委托的方法
1.1下面先从一个简单的例子比较命名方法,匿名方法和Lambda 表达式三种实现委托的方法
(1)申明一个委托,当然这只是一个最简单的委托,没有参数和返回值,所以可以使用Action 委托
(2)创建一个静态方法,以作为参数实例化委托
(3)在主函数中添加代码
输出
命名方式
匿名方法
Lambda 表达式
1.2说明
通过这个例子可以看出,三种方法中命名方式是最麻烦的,代码也很臃肿,而匿名方法和Lambda 表达式则直接简洁很多。这个例子只是实现最简单的委托,没有参数和返回值,事实上Lambda 表达式较匿名方法更直接,更具有表达力。本文就不详细介绍Lambda表示式了,可以在MSDN上详细了解http://msdn.microsoft.com/zh-cn/library/bb397687.aspx那么Lambda表达式除了书写方式和匿名方法不同之外,还有什么不一样的地方吗?众所周知,.Net工程编译生成的输出文件是程序集,而程序集中的代码并不是可以直接运行的本机代码,而是被称为CIL(IL和MSIL都是曾用名,本文采用CIL)的中间语言。
原理图如下:
因此可以通过CIL代码研究C#语言的实现方式。(本文采用ildasm.exe查看CIL代码)
2.从CIL代码比较匿名方法和Lambda表达式区别
2.1C#代码
为了便于研究,将之前的例子拆分为两个不同的程序,唯一区别在于主函数
代码1采用匿名方法
代码2采用Lambda 表达式
2.2查看代码1程序集CIL代码
用ildasm.exe查看代码1生成程序集的CIL代码
可以分析出CIL中类结构:
静态函数CIL代码
主函数
2.3查看代码2程序集CIL代码
用ildasm.exe查看代码2生成程序集的CIL代码
通过比较发现和代码1生成程序集的CIL代码完全一样。
2.4分析
可以清楚的发现在CIL代码中有一个静态的方法<Main>b__0,其内容就是匿名方法和Lambda 表达式语句块中的内容。在主函数中通过<Main>b__0实例委托,并调用。
2.5结论
无论是用匿名方法还是Lambda 表达式实现的委托,其本质都是完全相同。他们的原理都是在C#语言编译过程中,创建了一个静态的方法实例委托的对象。也就是说匿名方法和Lambda 表达式在CIL中其实都是采用命名方法实例化委托。
C#在通过匿名函数实现委托时,需要做以下步骤
(1)一个静态的方法(<Main>b__0),用以实现匿名函数语句块内容
(2)用方法(<Main>b__0)实例化委托
匿名函数在CIL代码中实现的原理图
3.从CIL代码研究带有参数的委托
3.1C#代码
为了便于研究采用匿名方法实现委托的方式,将代码改为:
(1)将委托改为
(2)将主函数改为
输出结果
Just for test
3.2查看CIL代码
静态函数
主函数
3.3分析
可以看出与上一节的例子唯一不同的是CIL代码中生成的静态函数需要传递一个string对象作为参数。
3.4结论
委托是否带有参数对于C#实现基本没有影响。
4.从CIL代码研究匿名函数捕获变量和闭包的实质
匿名函数不同于命名方法,可以访问它门外围作用域的局部变量和环境。本文采用了一个例子说明匿名函数(Lambda 表达式)可以捕获外围变量。而只要匿名函数有效,即使变量已经离开了作用域,这个变量的生命周期也会随之扩展。这个现象被称为闭包。
4.1C#代码
代码如下:
(1)定义一个委托
(2)在主函数中添加中添加代码
输出结果
110
4.2查看CIL代码
分析类结构
分析Program::Main方法(主函数)
分析<>c__DisplayClass1::<Main>b__0方法
4.3分析
可以看到与之前的例子不同,CIL代码中创建了一个叫做<>c__DisplayClass1的类,在类中有一个字段public int32 t,和方法<Main>b__0,分别对应要捕获的变量和匿名函数的语句块。
从主函数可以分析出流程
(1)创建一个<>c__DisplayClass1实例对象
(2)将<>c__DisplayClass1实例对象的字段t赋值为10
(3)创建一个DelTest委托类的实例对象,将<>c__DisplayClass1实例对象的<Main>b__0方法传递给构造函数
(4)调用DelTest委托,并将100作为参数
这时就不难理解闭包现象了,因为C#其实用类的字段来捕获变量(无论值类型还是引用类型),所其作用域当然会随着匿名函数的生存周期而延长。
4.4结论
C#在通过匿名函数实现需要捕获变量的委托时,需要做以下步骤
(1)创建一个类(<>c__DisplayClass1)
(2)在类中根据将要捕获的变量创建对应的字段(public int32 t)
(3)在类中创建一个方法(<Main>b__0),用以实现匿名函数语句块内容
(4)创建类(<>c__DisplayClass1)的对象,并用其方法(<Main>b__0)实例化委托
闭包现象则是因为步骤(2),捕获变量的实现方式所带来的附加产物。
需要捕获变量的匿名函数在CIL代码中实现原理图
结论
C#在实现匿名函数(匿名方法和Lambda 表达式),是通过隐式的创建一个静态方法或者类(需要捕获变量时),然后通过命名方式创建委托。
本文到这里笔者已经完成了对匿名方法,Lambda 表达式和闭包的探索, 明白了这些都是C#为了方便用户编写代码而准备的“语法糖”,其本质并未超出.Net之前的范畴。
白话学习MVC(八)Action的执行二
概述
上篇博文《白话学习MVC(七)Action的执行一》介绍了ASP.NET MVC中Action的执行的简要流程,并且对TempData的运行机制进行了详细的分析,本篇来分析上一篇中遗留的【3-2、ActionInvoker.InvokeAction(ControllerContext, actionName)】部分的内容,其中包含了Action的执行、过滤器的执行、View的呈现(下节介绍)。
public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter {protected override void ExecuteCore() { //获取上次处理过程中没有被使用的TempData PossiblyLoadTempData(); try { //从路由数据中获取请求的Action的名字 string actionName = RouteData.GetRequiredString("action"); if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) { HandleUnknownAction(actionName); } } finally { //将TempData保存到Session中。等待之后将Session的key【__ControllerTempData】发送到响应流中! PossiblySaveTempData(); } } }
详细分析
概述中的红色字体部分,也就是我们上一节中遗留的代码段,它实现了Action的执行。现在我们就来通过MVC源代码分析此段代码所涉及的所有部分。
public abstract class Controller : ControllerBase, IActionFilter, IAuthenticationFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter, IAsyncController, IAsyncManagerContainer { private IActionInvoker _actionInvoker; public IActionInvoker ActionInvoker { get { //ActionInvoker的InvokeAction方法就是执行Action的调用。 //可见,此处又有一个扩展点,设置自定义的ActionInvoker(即:在激活Controller后,执行该控制器实例的ActionInvoker属性,为属性赋值即可)。 if (_actionInvoker == null) { _actionInvoker = CreateActionInvoker(); } return _actionInvoker; } set { _actionInvoker = value; } } protected override void ExecuteCore() { PossiblyLoadTempData(); try { //从路由数据中获取请求的Action的名字(路由系统从请求地址中获取) string actionName = RouteData.GetRequiredString("action"); //ActionInvoker是Controller类中的一个属性,该属性默认返回的是一个AsyncControllerActionInvoker对象 if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) { HandleUnknownAction(actionName); } } finally { PossiblySaveTempData(); } } protected virtual IActionInvoker CreateActionInvoker() {//对于Resolver,只能根据类型反射创建实例,接口和抽象类都是返回null,所以下面的代码返回的是一个AsyncControllerActionInvoker对象!(MVC3中是直接返回一个ControllerActionInvoker) //AsyncControllerActionInvoker不只是异步的,他还包括了同步。因为他继承自ControllerActionInvoker类,并实现了IAsyncActionInvoker接口。 //这里就有疑问了,既然接口和抽象类都不能创建实例且返回null,那为什么还以接口为参数呢? return Resolver.GetService<IAsyncActionInvoker>() ?? Resolver.GetService<IActionInvoker>() ?? new AsyncControllerActionInvoker(); } }
上述代码中,红色字体部分中的ActionInvoker是Controller类的一个属性,该属性返回的是私有IActionInvoker类型的字段_actionInvoker的值,如果_actionInvoker不等于null,则返回字段_actionInvoker的值,否则创建一个 AsyncControllerAtionInvoker对象赋值给_actionInvoker字段并返回。所以,在我们没有设置自定义ActionInvoker时,默认这个ActionInvoker是一个AsyncControllerActonInvoker对象。即:执行AsyncControllerActonInvoker对象的InvokeAction方法来完成Action的执行!
扩展:此处我们可以创建一个自定义的ActionInvoker,然后使用自定义的ActionInvoker来实现Action的执行!
1、创建自定义一个ActionInvoker(实现IActionInvoker接口的类或者直接继承AsyncControllerActonInvoker类)。
2、创建好自定义的ActionInvoker之后,就需要将我们的ActionInvoker设置到系统中,就是通过请求的控制器HomeController的基类Controller的这个ActionInvoker属性来进行设置。所以,我们就需要在HomeController被激活时,直接执行该控制器实例的ActionInvoker属性来设置,而控制器的激活是在一个ControllerActivator的Create方法中完成的,ControllerActivator的选择又是ControllerFactory来做的!
上面指出两部分内容,一、在默认情况下的ActionInvoker(AsyncControllerActonInvoker);二、使用自定义ActionInvoker。我们所提到的ActionInvoker都是泛指实现了IActionInvoker接口的类,而上述两个中情况【默认ActionInvoker(AsyncControllerActonInvoker)】和【自定义ActionInvoker】便是实现了IActionInvoker接口,并实现了该接口中唯一的一个方法InvokeAction,而实现的这个方法中包含了对Action执行的所有操作,下面就来看看默认情况下ActionInvoker(AsyncControllerActonInvoker)的InvokeAction方法中是如何定义的!其实,自定义ActionInvoker的InvokeAction方法也是仿照AsyncControllerActonInvoker类来实现的。
public class ControllerActionInvoker : IActionInvoker { public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) { //ControllerDescriptor封装描述控制器的信息,如控制器的名称、类型和操作。 ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext); //FindAction方法:找到要执行的那么Action,并将该Action的相关信息封装在ActionDescriptor中。 ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName); if (actionDescriptor != null) { //GetFilters方法:获取应用在Action上的所有过滤器,并封装到一个FilterInfo对象中。 FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor); try { //Authorize授权过滤器需要实现IAuthorizationFilter接口,该接口有一个方法:OnAuthorization //循环执行应用在Actio上所有Authorize授权过滤器的OnAuthorization方法,定义如果不满足过滤器条件,则需要创建一个ActionResult复制给Result属性 //AuthorizeAttribute是MVC封装好的一个授权过滤器,从Cookie中获取信息,检查是否授权成功,可参考定义自己的授权管理器 AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor); if (authContext.Result != null) { //没有通过Authorize授权过滤器,直接根据自定义的ActionResult进行View的呈现!
//View的呈现(下一节介绍) InvokeActionResult(controllerContext, authContext.Result); } else { //ValidateRequest,该值指示是否为此请求启用请求验证 //是否对请求必须验证,默认为true,该属性定义在ControllerBase类中 if (controllerContext.Controller.ValidateRequest) { //ValidateRequest应该是检查XSS威胁之类的,在模型绑定请求中获取值前进行处理。 ValidateRequest(controllerContext); } //获取Action方法参数的值(模型绑定) IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor); //执行【方法过滤器】(实现IActionFilter接口)并执行Action内代码 ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters); //执行【结果过滤器】(实现IResultFilter接口),再做View的呈现。 InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result); } } catch (ThreadAbortException) { throw; } catch (Exception ex) { //执行异常过滤器 ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex); if (!exceptionContext.ExceptionHandled) { throw; }
//根据异常过滤器中定义的ActionResult进行View的呈现 InvokeActionResult(controllerContext, exceptionContext.Result); } return true; } // notify controller that no method matched return false; } }
上述代码中已添加了详细的注释,大致流程为:首先,根据【控制器信息】和【Action的Name】从被请求控制器的众多Action中找到要访问的Action,然后再执行应用在Action上的过滤器,最后根据ActionResult再进行View的呈现(下一节介绍)。
扩展:此处ControllerActionInvoker是MVC4中的,MVC5中新添加Authorizetion过滤器,并且这个过滤器的执行模式和Authorizetion过滤器是一样。对于Authentic过滤器,它需要实现IAuthenticationFilter接口,该接口中有两个方法:OnAuthentication和OnAuthenticationChallenge,执行顺序为:【Authentic过滤器的OnAuthentication方法】—>【Action过滤器的执行】—>【Action内代码的执行】—>【Authentic过滤器的OnAuthenticationChallenge方法】—>【Result过滤器的执行】—>【View的呈现】,所以就目前看来,通过这个过滤器也就可以在Action执行前且View呈现之前进行一些操作,从而可增强扩展性!
InvokeAction方法解析
1、ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
在控制器HomeController对象的所有方法中,找到当前请求的Action(控制器对象的一个方法)。
上述代码中,其实就是通过HomeController实例利用反射获取到所有的方法(包括父类中的方法),然后就是进行筛选,首先通过判断得到的方法是否是Controller类实例的方法,从而将HomeController父类中的方法过滤掉;之后再根据方法的名字来做判断,从而将方法名字不是请求的actionName的方法过滤掉;再之后判断应用在Action方法上的特性是否和客户端使用的 HTTP 数据传输方法一致,将不符合Http数据传输方法的Action过滤掉;再再之后,当符合条件的Action方法只有一个时,就将该Action方法返回,即:得到了指定的Action。最后将得到的Action方法封装到一个继承自ActionDescriptor类的ReflectedActionDescriptor对象中。
扩展:由上面介绍可知,其实对Action方法的查找和筛选都是在ActionMethodSelector中进行的,MVC5中的ActionMethodSelector虽然大体上流程是和MVC4相同的,但是具体实现上还是有点差异,有兴趣的可以看一下。
2、FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
获取应用在Action方法上的所有过滤器,并将封装到一个FilterInfo对象中。这些过滤器有:ActionFilter、AuthorizationFilter、ExceptionFilter、ResultFilter,另外在MVC5中又新添加了一个AuthenticationFilter过滤器。
上述代码中,实现了获取过滤器,而过滤器可以通过4种方式添加:1、Global.asax中的RegisterGlobalFilters方法,Scope=10;2、在控制器HomeController上以特性的方法添加,Scope=20;3、在Action上以特性的方式添加,Scope=30;4、控制器HomeController本身也是过滤器,它实现了各过滤器接口,Scope=0;
针对以上的4中添加方法,【1】直接通过GlobalFilterCollection集合来获取,应为GlobalFilterCollection实现了IEnumerable接口、【2】【3】通过FilterAttributeFilterProvider对象的GetFilters方法来获取、【4】通过ControllerInstanceFilterProvider对象的GetFilters方法来获取。
所以,整个流程为:遍历执行各【过滤器的提供器】的GetFilters方法,从而得到所有过滤器且过滤器按照Scope值从小到大排列,然后再从后向前执行来对不允许重复使用的过滤器进行去重(只保留Scope值大的过滤器),如果允许重复使用的话(AllowMutiple=true),表示允许重复使用该过滤器,则不执行去重。最终将得到的过滤器按照过滤器类型(按接口不同)分类封装到FilterInfo对象中。
更正:FilterProviderCollection类的AllowMultiple方法中【if(mvcFilter==null){true}】,也表示该过滤器为控制器本身。因为Controller类只实现了过滤器接口,而没有实现IMvcFilter接口或继承实现了IMvcFilter接口的类。
扩展:如有兴趣可以看一下MVC5中过获取过滤器代码
3、AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
执行Authorize授权过滤器,其实就是执过滤器的OnAuthorization方法。AuthorizeAttribute是一个MVC的授权过滤器,可参考定义自己的授权过滤器。授权过滤器本质上是去读取cookie,检查cookie中相应的值是否和授权过滤器中设置的一致(可以用来做登录之后才能访问某页面的功能)。
上述代码中,遍历所有的Authorize授权过滤器并执行其OnAuthorization方法,在OnAuthorization方法中,如果请求不满足条件,则创建一个ActionResult对象并赋值给AuthorizationContext对象的Result属性,之后直接使用该ActionResult进行View的呈现。
在MVC中,AuthorizeAttribute是微软定义的一个授权过滤器,它的OnAuthorization方法中规定,如果不满足条件的话,就跳转到登录页面(在WebConfig文件中配置)。
4、IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
模型绑定,获取Action方法参数对象的实参。详细请看:白话学习MVC(六)模型绑定
5、ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
执行【方法过滤器】(实现IActionFilter接口)并执行Action方法内的代码
上述代码,首先循环执行所有Action过滤器的ActionExecuting方法,然后执行Action方法内的代码,最后再循环执行所有的Action过滤器的ActionExecuted方法。此过程的过滤器中,如果不满足过滤器的要求,则直接利用自定义的ActionResult对象进行View的呈现!
补充:过滤器的ActionExecuting方法和ActionExecuted方法的执行是按照一条龙的顺序执行的。
此图摘自:http://www.cnblogs.com/artech/archive/2012/08/06/action-filter.html
重要:InvokeActionMethodWithFilters方法中的那句碉堡的代码实现了一条龙的方式去【OnActionExecuting】和【OnActionExecuted】方法,必须要好好学习下!!!
6、InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
执行【结果过滤器】(实现IResultFilter接口),再做View的呈现。
上述代码中,先循环执行所有的Result过滤器的OnResultExecuting方法,然后执行View的呈现,再循环执行所有的Result过滤器的OnResultExecuted方法。执行流程也是一条龙的方方式!
以上所有就是Action执行的全部,如果不符之处,请指正!由以上的执行可知【View的呈现】是通过ControllerActionInvoker类的InvokeActionResult方法来实现的,下一篇就来详细分析View的呈现相关的知识!
jQuery读取和设定KindEditor的值
在使用Kindeditor的时候,想要利用Ajax传值,但是通过editor封装的方法是行不通的,原因在于编辑器我们是放在另一个jsp页面,通过iframe来加载的,同时这个iframe的display=”none”的,要通过一个事件来触发。
<iframe src="../common/editor.jsp" frameborder="0" scrolling="no" style="margin: 0"
width="100%" height="300" name="zwFrame" id="zwFrameId"></iframe>
既然原本方法行不通,那我就只好通过jQuery来获取了。首先我想到的是读取内容“textarea”里面的内容,即:$(“#editor”).html(),但是这样是获取不到的。于是我想通过获取iframe里面的内容来获取,也没有获取到,最后通过firefox的debug查看找到最终结果:
从上面这个图中可以看出,要获取“今天天气很好”这个内容,我们只需要获取指定body里面的内容即可。
处理流程:首先获取最外层的iframe,通过iframe取里面的子元素iframe,在进入一层取里面的body即可。如下:
var editorText = $(window.frames['zwFrame'].document).find("iframe").contents().find("body"); var contents = editorText.html();
其中contents():查找匹配元素内部所有的子节点(包括文本节点)。如果元素是一个iframe,则查找文档内容。
下面提供几种获取iframe里面元素内容的方法:
$(document.getElementsByTagName("iframe")[0].contentWindow.document.body).html();
显示iframe中body元素的内容
$(document.getElementById("iframeId").contentWindow.document.body).html();
获取iframe中textarea元素的内容
$(window.frames["iframeName"].document).find("#textareaId").html();