C#委托和事件本質
C#中委托和事件是很重要的組成部分,而掌握委托和事件的本質將必不可少。為了能探秘本質,寫了如下代碼
復制代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Water water = new Water();
water.WaterBoils += water_WaterBoils;
water.WaterBoils += water_WaterBoils2;
water.DegreeRise();
Console.ReadLine();
}
static void water_WaterBoils(object sender, WaterBoilsEventArgs args)
{
Console.WriteLine(string.Format("sender:{0}", sender.ToString()));
Console.WriteLine(string.Format("args:{0}", args.CurentDegree.ToString()));
}
public static void water_WaterBoils2(object sender, WaterBoilsEventArgs args)
{
Console.WriteLine(string.Format("sender_2:{0}", sender.ToString()));
Console.WriteLine(string.Format("args_2:{0}", args.CurentDegree.ToString()));
}
}
public delegate void WaterBoilsEventHandler(object sender,WaterBoilsEventArgs args);
public class WaterBoilsEventArgs : EventArgs
{
public int CurentDegree { get; set; }
}
public class Water
{
public event WaterBoilsEventHandler WaterBoils;
public int curentDegree = 0;
public void DegreeRise()
{
for(var n = 99;n<200;n++)
{
Thread.Sleep(1000);
if(n>=100)
{
if(WaterBoils!=null)
{
WaterBoils(this,new WaterBoilsEventArgs(){CurentDegree = n});
//WaterBoils.Invoke(this, new WaterBoilsEventArgs() { CurentDegree = n });
}
}
}
}
}
}
復制代碼
介紹一下這段代碼:定義了一個委托WaterBoilsEventHandler,一個類Water,Water的DegreeRise方法觸發WaterBoils事件,一個事件參數WaterBoilsEventArgs ,一個Main方法.
既然是要看委托了事件的本質,所以借助反編譯工具(Reflector)查看編譯後的代碼片段:
首先來看一下委托編譯以後的代碼:
復制代碼
.class public auto ansi sealed WaterBoilsEventHandler
extends [mscorlib]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
{
}
.method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(object sender,
class ConsoleApplication1.WaterBoilsEventArgs args, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
{
}
.method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
{
}
.method public hidebysig newslot virtual instance void Invoke(object sender, class ConsoleApplication1.WaterBoilsEventArgs args)
runtime managed
{
}
}
復制代碼
從反編譯IL代碼中可以看出:
1.自定義委托的本質是一個類,具有Invoke,BeginInvoke,Endinvoke方法分別來支持同步和異步的執行,Invoke無返回值;BeginInvoke返回IAsyncResult,IAsyncResult可以作為EndInvoke的參數來結束執行。
2.自定義委托繼承自MulticastDelegate,使自定義委托具有“組播”的能力,也就是可以綁定多個委托;(MulticastDelegate類又繼承自Delegate)
下面來看看“組播”是怎麼實現的:MulticastDelegate內有兩個方法CombineImpl和RemoveImpl分別完成委托的綁定和解綁定;內部有一個對象(使用中會轉換成object[])_invocationList存儲MulticastDelegate對象;還有其他的一些方法共同完成。
從上邊可以看出,委托是可以綁定執行多個方法的,那為什麼還要事件呢,我覺得這個問題可能從語言設計角度上講,委托的設計出發點是規避猶如在C、C++等中的指針變量,委托具把這個地址進行了包裝,反編譯Delegate類,發現有如下成員
QQ截圖20140712133307 _methodPtr:方法的指針,一個Delegate對象維護了一個方法的引用地址
在使用委托的是否發現有有悖的地方:《 C# 與 .Net 3.5 高級程序設計第四版》有如下解釋:
“如果我們沒有把委托成員變量定義為私有的,調用者就可以直接訪問委托對象,這樣調用者就可以把變量賦值為新的委托對象(實際上也就是刪除了當前要調用的方法列表),更糟糕的是,調用者可以直接調用委托的調用列表。”
這裡就有一個比較嚴重的問題:1.比如兩個對象去注冊,A已經注冊了一個方法,B去注冊的時候把A注冊的方法給刪除了,並且B還可以操作到A注冊的方法。這種不友好是不能忍受的。
C#提供了event關鍵字來為我們解決問題,使用過event的童鞋們都可能有印象,在聲明事件以外的其他類中,如果調用事件,只能干兩件事情:
1.綁定方法引用,
2.解綁方法引用。
這是對事件的限制。
順著這條思路,看看C#中事件是怎麼完成的。
首先看一看,Water類的反編譯結果
復制代碼
.class public auto ansi beforefieldinit Water
extends [mscorlib]System.Object
{
.event ConsoleApplication1.WaterBoilsEventHandler WaterBoils
{
.addon instance void ConsoleApplication1.Water::add_WaterBoils(class ConsoleApplication1.WaterBoilsEventHandler)
.removeon instance void ConsoleApplication1.Water::remove_WaterBoils(class ConsoleApplication1.WaterBoilsEventHandler)
}
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
}
.method public hidebysig instance void DegreeRise() cil managed
{
}
.field public int32 curentDegree
.field private class ConsoleApplication1.WaterBoilsEventHandler WaterBoils
}
復制代碼
add_WaterBoils方法的方法定義如下:
復制代碼
public void add_WaterBoils(WaterBoilsEventHandler value)
{
WaterBoilsEventHandler handler2;
WaterBoilsEventHandler waterBoils = this.WaterBoils;
do
{
handler2 = waterBoils;
WaterBoilsEventHandler handler3 = (WaterBoilsEventHandler) Delegate.Combine(handler2, value);
waterBoils = Interlocked.CompareExchange<WaterBoilsEventHandler>(ref this.WaterBoils, handler3, handler2);
}
while (waterBoils != handler2);
}
復制代碼
remove_WaterBoils方法的方法定義如下
復制代碼
public void remove_WaterBoils(WaterBoilsEventHandler value)
{
WaterBoilsEventHandler handler2;
WaterBoilsEventHandler waterBoils = this.WaterBoils;
do
{
handler2 = waterBoils;
WaterBoilsEventHandler handler3 = (WaterBoilsEventHandler) Delegate.Remove(handler2, value);
waterBoils = Interlocked.CompareExchange<WaterBoilsEventHandler>(ref this.WaterBoils, handler3, handler2);
}
while (waterBoils != handler2);
}
復制代碼
可以看出:
1.增加了兩個方法:add_WaterBoils 和 remove_WaterBoils;
2.還增加了一個WaterBoilsEventHandler 類型的 私有變量 WaterBoils且與聲明的事件對象名相同
這裡容易產生一點迷惑:.event ConsoleApplication1.WaterBoilsEventHandler WaterBoils 有點像一個內部類的結構,且這個“類”具有兩個方法。暫時把這個迷惑放下。
3.add_WaterBoils和remove_WaterBoils方法都是對私有字段WaterBoils的維護。
知道這些後,再開看看注冊的時候是怎麼調用的:
復制代碼
private static void Main(string[] args)
{
Water water = new Water();
water.WaterBoils += new WaterBoilsEventHandler(Program.water_WaterBoils);
water.WaterBoils += new WaterBoilsEventHandler(Program.water_WaterBoils2);
water.DegreeRise();
Console.ReadLine();
}
復制代碼
這是使用的C#代碼方式,看不出來什麼結果,查看IL代碼如下:
復制代碼
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 3
.locals init (
[0] class ConsoleApplication1.Water water)
L_0000: nop
L_0001: newobj instance void ConsoleApplication1.Water::.ctor()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: ldnull
L_0009: ldftn void ConsoleApplication1.Program::water_WaterBoils(object, class ConsoleApplication1.WaterBoilsEventArgs)
L_000f: newobj instance void ConsoleApplication1.WaterBoilsEventHandler::.ctor(object, native int)
L_0014: callvirt instance void ConsoleApplication1.Water::add_WaterBoils(class ConsoleApplication1.WaterBoilsEventHandler)
L_0019: nop
L_001a: ldloc.0
L_001b: ldnull
L_001c: ldftn void ConsoleApplication1.Program::water_WaterBoils2(object, class ConsoleApplication1.WaterBoilsEventArgs)
L_0022: newobj instance void ConsoleApplication1.WaterBoilsEventHandler::.ctor(object, native int)
L_0027: callvirt instance void ConsoleApplication1.Water::add_WaterBoils(class ConsoleApplication1.WaterBoilsEventHandler)
L_002c: nop
L_002d: ldloc.0
L_002e: callvirt instance void ConsoleApplication1.Water::DegreeRise()
L_0033: nop
L_0034: call string [mscorlib]System.Console::ReadLine()
L_0039: pop
L_003a: ret
}
復制代碼
重點分析一下 L_0009 到 L_0014 的這三行代碼:
L_0009 :把water_WaterBoils方法的指針推送到計算堆棧上
L_000f :創建一個WaterBoilsEventHandler委托對象
L_0014 :調用add_WaterBoils,並把結果推送到計算堆棧上
同時請注意:“ConsoleApplication1.Water::add_WaterBoils”這句代碼,add_WaterBoils是ConsoleApplication1.Water類對象的方法,上邊的迷惑,看著像一個“內部類”,實則沒那關系。
這段代碼讓我們明白:事件的注冊是調用編譯生成的方法 add_WaterBoils 把 委托中帶有的方法引用地址 維護到編譯生成的私有屬性 WaterBoils 中去了。
所以event就是語法糖,節省了編寫代碼的時間,並且事件的觸發使用專門的事件來處理,顯出語言本身的完整,真正的實現是編譯器生成委托對象和其他處理代碼實現的。