(点击上方蓝字,可快速 委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和函数指针类似的功能,那就是提供了程序回调指定方法的机制。 在委托内部,包含了一个指向某个方法的指针(这一点上委托实现机制和C++的函数指针一致),为何称其为安全的呢?因此委托和其他.NET成员一样是一种类型,任何委托对象都是继承自Systm.Dlgat的某个派生类的一个对象,下图展示了在.NET中委托的类结构: 从上图也可以看出,任何自定义的委托都继承自基类Systm.Dlgat,在这个类中,定义了大部分委托的特性。那么,下面可以看看在.NET中如何使用委托: //定义的一个委托 publicdlgatvoidTstDlgat(inti); publicclassProgram { publicstaticvoidMain(string[]args) { //定义委托实例 TstDlgattd=nwTstDlgat(PrintMssag); //调用委托方法 td(0); td.Invok(1); Consol.RadKy(); } publicstaticvoidPrintMssag(inti) { Consol.WritLin(这是第{0}个方法!,i.ToString()); } } 运行结果如下图所示:
上述代码中定义了一个名为TstDlgat的新类型,该类型直接继承自Systm.MulticastDlgat,而且其中会包含一个名为Invok、BginInvok和EndInvok的方法,这些步骤都是由C#编译器自动帮我们完成的,可以通过Rflctor验证一下如下图所示: 需要注意的是,委托既可以接受实例方法,也可以接受静态方法(如上述代码中接受的就是静态方法),其区别我们在1.2中详细道来。最后,委托被调用执行时,C#编译器可以接收一种简化程序员设计的语法,例如上述代码中的:td(1)。但是,本质上,委托的调用其实就是执行了在定义委托时所生成的Invok方法。 1.2委托回调静态方法和实例方法有何区别? 首先,我们知道静态方法可以通过类名来访问而无需任何实例对象,当然在静态方法中也就不能访问类型中任何非静态成员。相反,实例方法则需要通过具体的实例对象来调用,可以访问实例对象中的任何成员。 其次,当一个实例方法被调用时,需要通过实例对象来访问,因此可以想象当绑定一个实例方法到委托时必须同时让委托得到实例方法的代码段和实例对象的信息,这样在委托被回调的时候.NET才能成功地执行该实例方法。 下图展示了委托内部的主要结构: ①_targt是一个指向目标实例的引用,当绑定一个实例方法给委托时,该参数会作为一个指针指向该方法所在类型的一个实例对象。相反,当绑定一个静态方法时,该参数则被设置为null。 ②_mthodPtr则是一个指向绑定方法代码段的指针,这一点和C++的函数指针几乎一致。绑定静态方法或实例方法在这个成员的设置上并没有什么不同。 Systm.MulticastDlgat在内部结构上相较Systm.Dlgat增加了一个重要的成员变量:_prv,它用于指向委托链中的下一个委托,这也是实现多播委托的基石。 1.3神马是链式委托? 链式委托也被称为“多播委托”,其本质是一个由多个委托组成的链表。回顾上面1.2中的类结构,Systm.MulticastDlgat类便是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行。 下面看看在.NET中,如何申明一个链式委托: //定义的一个委托 publicdlgatvoidTstMulticastDlgat(); publicclassProgram { publicstaticvoidMain(string[]args) { //申明委托并绑定第一个方法 TstMulticastDlgattmd=nwTstMulticastDlgat(PrintMssag1); //绑定第二个方法 tmd+=nwTstMulticastDlgat(PrintMssag2); //绑定第三个方法 tmd+=nwTstMulticastDlgat(PrintMssag3); //调用委托 tmd(); Consol.RadKy(); } publicstaticvoidPrintMssag1() { Consol.WritLin(调用第1个PrintMssag方法); } publicstaticvoidPrintMssag2() { Consol.WritLin(调用第2个PrintMssag方法); } publicstaticvoidPrintMssag3() { Consol.WritLin(调用第3个PrintMssag方法); } } 其运行结果如下图所示:
可以看到,调用头部的委托导致了所有委托方法的执行。通过前面的分析我们也可以知道:为委托+=增加方法以及为委托-=移除方法让我们看起来像是委托被修改了,其实它们并没有被修改。事实上,委托是恒定的。在为委托增加和移除方法时实际发生的是创建了一个新的委托,其调用列表是增加和移除后的方法结果。 另一方面,+=或-=这是一种简单明了的写法,回想在WindowsForm或者ASP.NETWbForms开发时,当添加一个按钮事件,VS便会自动为我们生成类似的代码,这样一想是不是又很熟悉了。 现在,我们再用一种更简单明了的方法来写: TstMulticastDlgattmd=PrintMssag1; tmd+=PrintMssag2; tmd+=PrintMssag3; tmd(); 其执行结果与上图一致,只不过C#编译器的智能化已经可以帮我们省略了很多代码。 最后,我们要用一种比较复杂的方法来写,但是却是链式委托的核心所在: TstMulticastDlgattmd1=nwTstMulticastDlgat(PrintMssag1); TstMulticastDlgattmd2=nwTstMulticastDlgat(PrintMssag2); TstMulticastDlgattmd3=nwTstMulticastDlgat(PrintMssag3); //核心本质:将三个委托串联起来 TstMulticastDlgattmd=tmd1+tmd2+tmd3; tmd.Invok(); 我们在实际开发中经常使用第二种方法,但是却不能不了解方法三,它是链式委托的本质所在。 1.4链式委托的执行顺序是怎么样的? 前面我们已经知道链式委托的基本特性就是一个以委托组成的链表,而当委托链上任何一个委托方法被调用时,其后面的所有委托方法都将会被依次地顺序调用。那么问题来了,委托链上的顺序是如何形成的?这里回顾一下上面1.3中的示例代码,通过Rflctor反编译一下,一探究竟: 从编译后的结果可以看到,+=的本质又是调用了Dlgat.Combin方法,该方法将两个委托链接起来,并且把第一个委托放在第二个委托之前,因此可以将两个委托的相加理解为Dltgat.Combin(Dlgata,Dlgatb)的调用。我们可以再次回顾Systm.MulticastDlgat的类结构: 其中_prv成员是一个指向下一个委托成员的指针,当某个委托被链接到当前委托的后面时,该成员会被设置为指向那个后续的委托实例。.NET也是依靠这一个引用来逐一找到当前委托的所有后续委托并以此执行方法。 那么,问题又来了?程序员能够有能力控制链式委托的执行顺序呢?也许我们会说,只要在定义时按照需求希望的顺序来依次添加就可以了。但是,如果要在定义完成之后突然希望改变执行顺序呢?又或者,程序需要按照实际的运行情况再来决定链式委托的执行顺序呢? 接下来就是见证奇迹的时刻: //申明委托并绑定第一个方法 TstMulticastDlgattmd=nwTstMulticastDlgat(PrintMssag1); //绑定第二个方法 tmd+=nwTstMulticastDlgat(PrintMssag2); //绑定第三个方法 tmd+=nwTstMulticastDlgat(PrintMssag3); //获取所有委托方法 Dlgat[]dls=tmd.GtInvocationList(); 上述代码调用了定义在Systm.MulticastDlgat中的GtInvocationList()方法,用以获得整个链式委托中的所有委托。接下来,我们就可以按照我们所希望的顺序去执行它们。 1.5可否定义有返回值方法的委托链? 委托的方法既可以是无返回值的,也可以是有返回值的,但如果多一个带返回值的方法被添加到委托链中时,我们需要手动地调用委托链上的每个方法,否则只能得到委托链上最后被调用的方法的返回值。 为了验证结论,我们可以通过如下代码进行演示: //定义一个委托 publicdlgatstringGtStringDlgat(); classProgram { staticvoidMain(string[]args) { //GtSlfDfindString方法被最后添加 GtStringDlgatmyDlgat1=GtDatTimString; myDlgat1+=GtTypNamString; myDlgat1+=GtSlfDfindString; Consol.WritLin(myDlgat1()); Consol.WritLin(); //GtDatTimString方法被最后添加 GtStringDlgatmyDlgat2=GtSlfDfindString; myDlgat2+=GtTypNamString; myDlgat2+=GtDatTimString; Consol.WritLin(myDlgat2()); Consol.WritLin(); //GtTypNamString方法被最后添加 GtStringDlgatmyDlgat3=GtSlfDfindString; myDlgat3+=GtDatTimString; myDlgat3+=GtTypNamString; Consol.WritLin(myDlgat3()); Consol.RadKy(); } staticstringGtDatTimString() { rturnDatTim.Now.ToString(); } staticstringGtTypNamString() { rturntypof(Program).ToString(); } staticstringGtSlfDfindString() { stringrsult=我是一个字符串!; rturnrsult; } } 其运行结果如下图所示:
从上图可以看到,虽然委托链中的所有方法都被正确执行,但是我们只得到了最后一个方法的返回值。在这种情况下,我们应该如何得到所有方法的返回值呢?回顾刚刚提到的GtInvocationList()方法,我们可以利用它来手动地执行委托链中的每个方法。 GtStringDlgatmyDlgat1=GtDatTimString; myDlgat1+=GtTypNamString; myDlgat1+=GtSlfDfindString; forach(vardlinmyDlgat1.GtInvocationList()) { Consol.WritLin(dl.DynamicInvok()); } 通过上述代码,委托链中每个方法的返回值都不会丢失,下图是执行结果:
1.6简述委托的应用场合 委托的功能和其名字非常类似,在设计中其思想在于将工作委派给其他特定的类型、组件、方法或程序集。委托的使用者可以理解为工作的分派者,在通常情况下使用者清楚地知道哪些工作需要执行、执行的结果又是什么,但是他不会亲自地去做这些工作,而是恰当地把这些工作分派出去。 这里,我们假设要写一个日志子系统,该子系统的需求是使用者希望的都是一个单一的方法传入日志内容和日志类型,而日志子系统会根据具体情况来进行写日志的动作。对于日志子系统的设计者来说,写一条日志可能需要包含一系列的工作,而日志子系统决定把这些工作进行适当的分派,这时就需要使用一个委托成员。 下面的代码展示了该日志子系统的简单实现方式: ①定义枚举:日志的类别 publicnumLogTyp { Dbug, Trac, Info, Warn, Error } ②定义委托,由日志使用者直接执行来完成写日志的工作 publicdlgatvoidLog(stringcontnt,LogTyptyp);
③定义日志管理类,在构造方法中为记录日志委托定义了默认的逻辑(这里采用了部分类的书写,将各部分的委托方法分隔开,便于理解) publicsaldpartialclassLogManagr:IDisposabl { privatTyp_ 日志管理类定义了一些列符合Log委托的方法,这些方法可以被添加到记录日志的委托对象之中,以构成整个日志记录的动作。在日后的扩展中,主要的工作也集中在添加新的符合Log委托定义的方法,并且将其添加到委托链上。 ④在Main方法中调用LogManagr的Log委托实例来写日志,LogManagr只需要管理这个委托,负责分派任务即可。 classProgram { staticvoidMain(string[]args) { //使用日志 using(LogManagrlogmanagr= nwLogManagr(Typ.GtTyp(LogSystm.Program),C:\\TstLog.txt)) { logmanagr.WritLog(新建了日志,LogTyp.Dbug); logmanagr.WritLog(写数据,LogTyp.Dbug); logmanagr.UsUTCTim(); logmanagr.WritLog(现在是UTC时间,LogTyp.Dbug); logmanagr.UsLocalTim(); logmanagr.WritLog(回到本地时间,LogTyp.Dbug); logmanagr.WritLog(发生错误,LogTyp.Error); logmanagr.WritLog(准备退出,LogTyp.Info); } Consol.RadKy(); } } 代码中初始化委托成员的过程既是任务分派的过程,可以注意到LogManagr的UsUTCTim和UsLocalTim方法都是被委托成员进行了重新的分配,也可以理解为任务的再分配。 下图是上述代码的执行结果,将日志信息写入了C:\TstLog.txt中:
二、事件基础 事件这一名称对于我们.NET码农来说肯定不会陌生,各种技术框架例如WindowsForm、ASP.NETWbForm都会有事件这一名词,并且所有的定义都基本相同。在.NET中,事件和委托在本质上并没有太多的差异,实际环境下事件的运用却比委托更加广泛。 2.1简述事件的基本使用方法 在Microsoft的产品文档上这样来定义的事件:事件是一种使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序为相应的事件添加可执行代码。设计和使用事件的全过程大概包括以下几个步骤: 下面我们来按照规范的步骤来展示一个通过控制台输出事件的使用示例: ①定义一个控制台事件ConsolEvnt的参数类型ConsolEvntArgs ///summary ///自定义一个事件参数类型 ////summary publicclassConsolEvntArgs:EvntArgs { //控制台输出的消息 privatstringmssag; publicstringMssag { gt { rturnmssag; } } publicConsolEvntArgs() :bas() { this.mssag=string.Empty; } publicConsolEvntArgs(stringmssag) :bas() { this.mssag=mssag; } } ②定义一个控制台事件的管理者,在其中定义了事件类型的私有成员ConsolEvnt,并定义了事件的发送方法SndConsolEvnt ///summary ///管理控制台,在输出前发送输出事件 ////summary publicclassConsolManagr { //定义控制台事件成员对象 publicvntEvntHandlrConsolEvntArgsConsolEvnt; ///summary ///控制台输出 ////summary publicvoidConsolOutput(stringmssag) { //发送事件 ConsolEvntArgsargs=nwConsolEvntArgs(mssag); SndConsolEvnt(args); //输出消息 Consol.WritLin(mssag); } ///summary ///负责发送事件 ////summary ///paramnam=args事件的参数/param protctdvirtualvoidSndConsolEvnt(ConsolEvntArgsargs) { //定义一个临时的引用变量,确保多线程访问时不会发生问题 EvntHandlrConsolEvntArgstmp=ConsolEvnt; if(tmp!=null) { tmp(this,args); } } } ③定义了事件的订阅者Log,在其中通过控制台时间的管理类公开的事件成员订阅其输出事件ConsolEvnt ///summary ///日志类型,负责订阅控制台输出事件 ////summary publicclassLog { //日志文件 privatconststringlogFil= C:\TstLog.txt;publicLog(ConsolManagrcm) { //订阅控制台输出事件 cm.ConsolEvnt+=this.WritLog; } ///summary ///事件处理方法,注意参数固定模式 ////summary ///paramnam=sndr事件的发送者/param ///paramnam=args事件的参数/param privatvoidWritLog(objctsndr,EvntArgsargs) { //文件不存在的话则创建新文件 if(!Fil.Exists(logFil)) { using(FilStramfs=Fil.Crat(logFil)){} } FilInfofi=nwFilInfo(logFil); using(StramWritrsw=fi.AppndTxt()) { ConsolEvntArgsca=argsasConsolEvntArgs; sw.WritLin(DatTim.Now.ToString(yyyy-MM-ddHH:mm:ss)+ +sndr.ToString()+ +ca.Mssag); } } } ④在Main方法中进行测试: classProgram { staticvoidMain(string[]args) { //控制台事件管理者 ConsolManagrcm=nwConsolManagr(); //控制台事件订阅者 Loglog=nwLog(cm); cm.ConsolOutput(测试控制台输出事件); cm.ConsolOutput(测试控制台输出事件); cm.ConsolOutput(测试控制台输出事件); Consol.RadKy(); } } 当该程序执行时,ConsolManagr负责在控制台输出测试的字符串消息,与此同时,订阅了控制台输出事件的Log类对象会在指定的日志文件中写入这些字符串消息。可以看出,这是一个典型的观察者模式的应用,也可以说事件为观察者模式提供了便利的实现基础。 2.2事件和委托有神马联系? 事件的定义和使用方式与委托极其类似,那么二者又是何关系呢?经常听人说,委托本质是一个类型,而事件本质是一个特殊的委托类型的实例。关于这个解释,最好的办法莫过于通过查看原代码和编译后的IL代码进行分析。 ①回顾刚刚的代码,在ConsolManagr类中定义了一个事件成员 publicvntEvntHandlrConsolEvntArgsConsolEvnt; EvntHandlr是.NET框架中提供的一种标准的事件模式,它是一个特殊的泛型委托类型,通过查看元数据可以验证这一点: [Srializabl] publicdlgatvoidEvntHandlrTEvntArgs(objctsndr,TEvntArgs);
正如上面代码所示,我们定义一个事件时,实际上是定义了一个特定的委托成员实例。该委托没有返回值,并且有两个参数:一个事件源和一个事件参数。而当事件的使用者订阅该事件时,其本质就是将事件的处理方法加入到委托链之中。 ②下面通过Rflctor来查看一下事件ConsolEvnt的IL代码(中间代码),可以更方便地看到这一点: 首先,查看EvntHandlr的IL代码,可以看到在C#编译器编译dlgat代码时,编译后是成为了一个class。 其次,当C#编译器编译vnt代码时,会首先为类型添加一个EvntHandlrT的委托实例对象,然后为其增加一对add/rmov方法用来实现从委托链中添加和移除方法的功能。 通过查看add_ConsolEvnt的IL代码,可以清楚地看到订阅事件的本质是调用Dlgat的Combin方法将事件处理方法绑定到委托链中。 L_:ldarg.0 L_:ldfldclass[mscorlib]Systm.EvntHandlr`1classConsolEvntDmo.ConsolEvntArgsConsolEvntDmo.ConsolManagr::ConsolEvnt L_:stloc.0 L_:ldloc.0 L_:stloc.1 L_:ldloc.1 L_a:ldarg.1 L_b:callclass[mscorlib]Systm.Dlgat[mscorlib]Systm.Dlgat::Combin(class[mscorlib]Systm.Dlgat,class[mscorlib]Systm.Dlgat) L_:castclass[mscorlib]Systm.EvntHandlr`1classConsolEvntDmo.ConsolEvntArgs L_:stloc.2 L_:ldarg.0 L_:ldfldaclass[mscorlib]Systm.EvntHandlr`1classConsolEvntDmo.ConsolEvntArgsConsolEvntDmo.ConsolManagr::ConsolEvnt Summary:事件是一个特殊的委托实例,提供了两个供订阅事件和取消订阅的方法:add_vnt和rmov_vnt,其本质都是基于委托链来实现。 2.3如何设计一个带有很多事件的类型? 多事件的类型在实际应用中并不少见,尤其是在一些用户界面的类型中(例如在WindowsForm中的各种控件)。这些类型动辄将包含数十个事件,如果为每一个事件都添加一个事件成员,将导致无论使用者是否用到所有事件,每个类型对象都将占有很大的内存,那么对于系统的性能影响将不言而喻。事实上,.NET的开发小组运用了一种比较巧妙的方式来避免这一困境。 Solution:当某个类型具有相对较多的事件时,我们可以考虑显示地设计订阅、取消订阅事件的方法,并且把所有的委托链表存储在一个集合之中。这样做就能避免在类型中定义大量的委托成员而导致类型过大。 下面通过一个具体的实例来说明这一设计: ①定义包含大量事件的类型之一:使用EvntHandlrList成员来存储所有事件 publicpartialclassMultiEvntClass { //EvntHandlrList包含了一个委托链表的容器,实现了多事件存放在一个容器之中的包装,它使用的是链表数据结构 privatEvntHandlrListvnts; publicMultiEvntClass() { //初始化EvntHandlrList vnts=nwEvntHandlrList(); } //释放EvntHandlrList publicvoidDispos() { vnts.Dispos(); } } ②定义包含大量事件的类型之二:申明多个具体的事件 publicpartialclassMultiEvntClass { #rgionvnt1 //事件1的委托原型 publicdlgatvoidEvnt1Handlr(objctsndr,EvntArgs); //事件1的静态Ky protctdstaticradonlyobjctEvnt1Ky=nwobjct(); //订阅事件和取消订阅 //注意:EvntHandlrList并不提供线程同步,所以加上线程同步属性 publicvntEvnt1HandlrEvnt1 { [MthodImpl(MthodImplOptions.Synchronizd)] add { vnts.AddHandlr(Evnt1Ky,valu); } [MthodImpl(MthodImplOptions.Synchronizd)] rmov { vnts.RmovHandlr(Evnt1Ky,valu); } } //触发事件1 protctdvirtualvoidOnEvnt1(EvntArgs) { vnts[Evnt1Ky].DynamicInvok(this,); } //简单地触发事件1,以便于测试 publicvoidRisEvnt1() { OnEvnt1(EvntArgs.Empty); } #ndrgion #rgionvnt2 //事件2的委托原型 publicdlgatvoidEvnt2Handlr(objctsndr,EvntArgs); //事件2的静态Ky protctdstaticradonlyobjctEvnt2Ky=nwobjct(); //订阅事件和取消订阅 //注意:EvntHandlrList并不提供线程同步,所以加上线程同步属性 publicvntEvnt2HandlrEvnt2 { [MthodImpl(MthodImplOptions.Synchronizd)] add { vnts.AddHandlr(Evnt2Ky,valu); } [MthodImpl(MthodImplOptions.Synchronizd)] rmov { vnts.RmovHandlr(Evnt2Ky,valu); } } //触发事件2 protctdvirtualvoidOnEvnt2(EvntArgs) { vnts[Evnt2Ky].DynamicInvok(this,); } //简单地触发事件2,以便于测试 publicvoidRisEvnt2() { OnEvnt2(EvntArgs.Empty); } #ndrgion } ③定义事件的订阅者(它对多事件类型内部的构造一无所知) publicclassCustomr { publicCustomr(MultiEvntClassvnts) { //订阅事件1 vnts.Evnt1+=Evnt1Handlr; //订阅事件2 vnts.Evnt2+=Evnt2Handlr; } //事件1的回调方法 privatvoidEvnt1Handlr(objctsndr,EvntArgs) { Consol.WritLin(事件1被触发); } //事件2的回调方法 privatvoidEvnt2Handlr(objctsndr,EvntArgs) { Consol.WritLin(事件2被触发); } } ④编写入口方法来测试多事件的触发 classProgram { staticvoidMain(string[]args) { using(MultiEvntClassmc=nwMultiEvntClass()) { Customrcustomr=nwCustomr(mc); mc.RisEvnt1(); mc.RisEvnt2(); } Consol.RadKy(); } } 最终运行结果如下图所示:
总结EvntHandlrList的用法,在多事件类型中为每一个事件都定义了一套成员,包括事件的委托原型、事件的订阅和取消订阅方法,在实际应用中,可能需要定义事件专用的参数类型。这样的设计主旨在于改动包含多事件的类型,而订阅事件的客户并不会察觉这样的改动。设计本身不在于减少代码量,而在于有效减少多事件类型对象的大小。
北京白癜风医院在哪里诊疗白癜风的医院
|