標籤:

c#中委託和事件?

總是感覺委託和事件沒什麼區別,調用事件不就是相當於調用多個委託么?


C#中的事件其實就是一個特殊的多播委託。比如寫一段代碼:

class Program
{
public delegate void SendHandler(string str);
public event SendHandler SendEvent;
static void Main(string[] args)
{ }
}

編譯後我們用ILDASM.EXE打開那個exe看看就會發現其實SendHandler委託被編譯為了一個叫做SendHandler的類,

SendEvent事件則是被編譯成了包含一個add_前綴和一個remove_前綴的的代碼段(倒三角)。

.event Program/SendHandler SendEvent
{
.addon instance void Program::add_SendEvent(class Program/SendHandler)
.removeon instance void Program::remove_SendEvent(class Program/SendHandler)
} // end of event Program::SendEvent

add_前綴的方法其實是通過調用Delegate.Combine()方法來實現的,組成了一個多播委託。remove_就是調用Delegate.Remove()方法,用於移除多播委託中的某個委託。

好了,前面的都不是廢話,你還會看到有一個SendEvent的欄位(蔚藍色菱形)

.field private class Program/SendHandler SendEvent

說事件是一個特殊的多播委託,那麼事件比較特殊的地方在於這裡,事件具有一個私有的委託類型的欄位,其存儲了對事件處理方法的引用。而add_前綴方法和remove_前綴方法起到的就是類似C#屬性訪問器中get_和set_方法的作用,使用事件訪問私有委託。

簡而言之,事件就是用來訪問私有的委託欄位,讓應用程序的代碼更加的安全。


在我的理解里

事件是類型對外暴露的只讀委託線性表,

一個事件

public event EventHandler Event1{add;remove}

相當於一個

piblic List& Event1{get;}

只不過額外增加了

Add(EventHandler value)

Remove(EventHandler value)

InvokeAll( params)

三個功能的快速入口。

你的問題

"總是感覺委託和事件沒什麼區別,調用事件不就是相當於調用多個委託么?"

就好像 " 感覺集合屬性和值是一樣的啊 調用屬性不就是調用裡面的多個值" 一樣。

似乎是問題自己就可以回答自己的。


除了語法和封裝性,補充兩點性能上的不同:

  1. 編譯器為事件默認生成的add/remove(即+=/-=)是線程安全和lock-free的。
  2. 事件可以不佔用對象內存。

關於第二點,比如Button這種UI控制項,動輒好幾十個事件,用委託或者默認的事件實現是很低效的,會浪費很多內存。要自己實現add、remove。只把add過的handler存在一個list中,這樣沒有使用的或用完註銷掉的事件是不會佔用內存的。


感覺官方說法可以搜索到,我就說理解。

先說結論(我的理解):聲明一個事件,類似於聲明一個進行封裝了的委託。

詳細的:

  • 委託

這個可以簡單的理解為:把方法當成方法的參數

看起來有點繞口,咱們先寫個「你好,世界!」

public void HelloWorld(string name)
{
ByEnglish(name);
}
public void ByEnglish(string name)
{
Console.WriteLine("Hello World By" + name);
}

先把這兩個方法寫出來,先別問為啥這麼寫

好了,現在產品經理打算整個中文版的,好,咱改!

public void ByChinese(string name)
{
Console.WriteLine("你好,世界!作者:" + name);
}

很明顯,上面的HelloWorld()得改了

public enum Language
{
English,CHinese
}
public void HelloWorld(string name,Language lan)
{
switch(lan)
{
case Language.English:ByEnglish(string name);break;
case Language.Chinese:ByChinese(string name);break;
}
}

差不多這樣的話可以滿足目前的需求了

現在突然又蹦出來個義大利人想弄出個義大利版(我不會義大利語,不貼了),按照上面的思路,大概也能整出來。

然後,產品經理說:這個方法咱要全球化,每個語言都要照顧到。(NMB)

按照上面的思路來,我覺得可以抽刀砍人了。

但是法律這玩意兒可怕喲,想到這個我的心兒就碎了。

所以咱們還是把「委託」掏出來吧!

委託上面的說的是:把方法當做方法的參數,大概就是這個樣子

public void HelloWorld(string name, 方法 方法名)

上委託,把上面代碼改了:

private static delegate void HelloWorldDelete(string name)
public void HelloWorld(string name, HelloWorldDelete chooseLanguage)
{
chooseLanguage(name);
}
private static void ByEnglish(string name)
{
Console.WriteLine("Hello World By" + name);
}
private static void ByChinese(string name)
{
Console.WriteLine("你好,世界!作者:" + name);
}
static void Main(string[] args)
{
HelloWorld("張三",ByChinese);
HelloWorld("ZhangThree",ByEnglish);
}

輸出:

你好,世界!作者:張三

Hello World By ZhangThree

再寫個綁定版的(這回只寫Main中的,因為其他的沒變):

static void Main(string[] args)
{
HelloWorldDelete delegateEnglish,delegateChinese;
delegateEnglish = ByEnglish;
delegateChinese = ByChinese;
HelloWorld("張三",delegateChinese);
HelloWorld("ZhangThree",delegateEnglish);
}

輸出不變。

換個姿勢綁定(只寫Main中的,因為其他的沒變):

static void Main(string[] args)
{
HelloWorldDelete delegateAll;
delegateAll = ByEnglish;
delegateAll += ByChinese;
HelloWorld("張三", delegateAll );
}

輸出:

Hello World By 張三

你好,世界!作者:張三

再換個體位(只寫Main中的,因為其他的沒變):

static void Main(string[] args)
{
HelloWorldDelete delegateAll;
delegateAll = ByEnglish;
//「=」表示賦值
delegateAll += ByChinese; //「+=」表示綁定,委託不能直接+=,會出現「未賦值
//什麼的錯誤」;相應的「-=」表示解除綁定
delegateAll ("張三");
}

  • 事件

寫代碼講究個「說學逗唱」,說錯了,講究個「面向對象」。

面向對象:封裝、繼承、多態。

咱們就先從封裝開始(我不會加二級標題):

封裝

把上面的封裝一下

public delegate void HelloWorldDelegate(string name);
public class HelloWorldClass
{
HelloWorldDelegate del;
public void HelloWorld(string name)
{
if(del != null) //有方法註冊了
del(name); //委託調用所有註冊的方法
}
}

class Program
{
private static void ByEnglish(string name)
{
Console.WriteLine("Hello World By" + name);
}
private static void ByChinese(string name)
{
Console.WriteLine("你好,世界!作者:" + name);
}

static void Main(string[] args)
{
HelloWorldClass hw = new HelloWorldClass();
hw.del = ByEnglish;
hw.del += ByChinese;
hw.HelloWorld("張三");
}
}

輸出:

Hello World By 張三

你好,世界!作者:張三

可是我們回頭一看,HelloWorldClass裡面的東西根本沒封裝住,很明顯del暴露了,不想暴露就得改成private,改了以後幾乎就沒用了。然後,我們很自然想到private欄位、public屬性,例如

public class Number
{
private int num;
public int Num
{
get { return; }
set { num = value; }
}
}

所以,把「事件」掏出來:

事件:事件至於委託,類似於屬性至於欄位。不管聲明成public還是protected,都是private的。

修改上面代碼

public delegate void HelloWorldDelegate(string name);
public class HelloWorldClass
{
public event HelloWorldDelegate del;
public void HelloWorld(string name)
{

del(name); //委託調用所有註冊的方法
}
}

class Program
{
private static void ByEnglish(string name)
{
Console.WriteLine("Hello World By" + name);
}
private static void ByChinese(string name)
{
Console.WriteLine("你好,世界!作者:" + name);
}

static void Main(string[] args)
{
HelloWorldClass hw = new HelloWorldClass();
hw.del = ByEnglish; //會出現編譯錯誤
hw.del += ByChinese; //直接+=就好
hw.HelloWorld("張三");
}
}

輸出:

你好,世界!作者:張三

先不寫了


隨便說兩句。

1、事件應該用引發(Raise),不應用調用(Invoke)。

2、調用多個委託直接用多播委託即可(MulticastDelegate),所有的委託類型都繼承於多播委託,所以都可以+和-來組合成多播委託。

3、事件的默認實現是個多播委託,事件可以使用+=和-=來註冊和移除委託,內部則是對一個多播委託直接操作。

4、事件是可以自定義+=和-=的行為的,也就是說可以不使用多播委託來實現,譬如說你可以用並發通知來實現事件,也就是把委託都丟到不同的Task裡面去一起調用。

5、委託和事件的區別在於,事件是個規約,委託是個實現(當然抽象上的委託也可以不是個具體的實現)。規約的含義是,我定義了這麼個語法,你可以通過+=和-=把委託掛載到這個東西(事件)上,當發生這個事件的時候,我會確保這些委託都被得到調用。但是具體是怎麼調用的,你不用關心。


不要想這麼多,安心學下去。


實際上事件是一種特殊的委託。當你在visual studio里看到某個事件例如event SampleHandler onXXXChanged; 時,你可以把滑鼠指針放在SampleHandler上然後右鍵查看它的原始定義,你會發現這個SampleHandler就是一個delegate類型,也就是說onXXXChanged實際上就是一個委託。

那麼為什麼要在這個onXXXChanged的定義前加上event關鍵字呢?

因為delegate可以支持的操作非常多,比如我們可以寫onXXXChanged += aaaFunc,把某個函數指針掛載到這個委託上面,但是我們也可以簡單粗暴地直接寫onXXXChanged = aaaFunc,讓這個委託只包含這一個函數指針。不過這樣一來會產生一個安全問題:如果我們用onXXXChanged = aaaFunc這樣的寫法,那麼會把這個委託已擁有的其他函數指針給覆蓋掉,這大概不是定義onXXXChanged的程序員想要看到的結果。

還有一個問題就是onXXXChanged這個委託應該什麼時候觸發(即調用它所包含的函數指針)。從面向對象的角度來說,XXX改變了這個事實(即onXXXChaned的字面含義)應該由包含它的那個對象來決定。但實際上我們可以從這個對象的外部環境調用onXXXChanged,這既產生了安全問題也不符合面向對象的初衷。

所以加了event關鍵字後我們對onXXXChanged這個委託有了兩個約束:

1. 只允許onXXXChanged += aaaFunc 和 onXXXChanged -= aaaFunc 這兩種使用方法,不允許onXXXChanged = aaaFunc 這個使用方法。

2. 我們只能在包含onXXXChanged的對象內部中調用這個委託,對象的外部環境中無法直接調用該委託,只能做1里的操作,即把某個函數指針掛載上去或者卸載下來。


我的理解是:事件是對委託的一種封裝。


在趙三本 @趙劼 中的《C# In Depth》一書中提到了一下,建議題主可以翻翻這本書,講的其中的區別。哈哈,我都忘了其中說了啥,雖然這是我第三遍看這本書……


委託變數可以在定義該委託變數的類型外部直接調用,一旦向類型外公開委託變數,什麼時候調用這個委託變數你是無法控制的

事件在定義該事件的類型外部只能添加或者移除處理程序,只有在定義事件的類型內部可以確定事件在什麼時候、什麼條件下執行,在外部是無法更改執行條件的


樓上把簡單問題複雜化了,或者說是層次太深,新手不容易理解

最簡單的說法是:可以把事件看成是委託的一個實例。

委託比作類:它定義了函數的簽名(接受什麼類型的參數 返回什麼類型的值)

事件比作委託new出來的一個實例,是具有該委託簽名的具體函數。

當然事件和實例也是有區別的:

1、事件這個東西 能容納很多個具體的函數(通過+= -= 增加刪除)。

2、事件有event關鍵字起到了保護作用不允許改變事件的引用。


class Program {

static void Main(string[] args) {

Publishser pub = new Publishser();

Subscriber sub = new Subscriber();

pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);

/*事件只可以+=和-=,而委託第一次可以使用=,後續可以使用+=,事件是委託的封裝。

gm.MakeGreet = EnglishGreeting;

gm.MakeGreet += ChineseGreeting;

*/

pub.DoSomething(); // 應該通過DoSomething()來觸發事件

pub.NumberChanged(100); // 但可以被這樣直接調用,對委託變數的不恰當使用

}

}

// 定義委託

public delegate void NumberChangedEventHandler(int count);

// 定義事件發布者

public class Publishser {

private int count;

public NumberChangedEventHandler NumberChanged; // 聲明委託變數

//public event NumberChangedEventHandler NumberChanged; // 聲明一個事件

public void DoSomething() {

// 在這裡完成一些工作 ...

if (NumberChanged != null) { // 觸發事件

count++;

NumberChanged(count);

}

}

}

// 定義事件訂閱者

public class Subscriber {

public void OnNumberChanged(int count) {

Console.WriteLine("Subscriber notified: count = {0}", count);

}

}

上面代碼定義了一個NumberChangedEventHandler委託,然後我們創建了事件的發布者Publisher和訂閱者Subscriber。當使用委託變數時,客戶端可以直接通過委託變數觸發事件,也就是直接調用pub.NumberChanged(100),這將會影響到所有註冊了該委託的訂閱者。而事件的本意應該為在事件發布者在其本身的某個行為中觸發,比如說在方法DoSomething()中滿足某個條件後觸發。通過添加event關鍵字來發布事件,事件發布者的封裝性會更好,事件僅僅是供其他類型訂閱,而客戶端不能直接觸發事件(語句pub.NumberChanged(100)無法通過編譯),事件只能在事件發布者Publisher類的內部觸發(比如在方法pub.DoSomething()中),換言之,就是NumberChanged(100)語句只能在Publisher內部被調用。

大家可以嘗試一下,將委託變數的聲明那行代碼注釋掉,然後取消下面事件聲明的注釋。此時程序是無法編譯的,當你使用了event關鍵字之後,直接在客戶端觸發事件這種行為,也就是直接調用pub.NumberChanged(100),是被禁止的。事件只能通過調用DoSomething()來觸發。這樣才是事件的本意,事件發布者的封裝才會更好。

就好像如果我們要定義一個數字類型,我們會使用int而不是使用object一樣,給予對象過多的能力並不見得是一件好事,應該是越合適越好。儘管直接使用委託變數通常不會有什麼問題,但它給了客戶端不應具有的能力,而使用事件,可以限制這一能力,更精確地對類型進行封裝。

NOTE:這裡還有一個約定俗稱的規定,就是訂閱事件的方法的命名,通常為「On事件名」,比如這裡的OnNumberChanged。

以上轉自: Jimmy Zhang - 博客園


如果站在委託和事件的最直接用途來看的話,委託只能綁定一個方法,每調用一次只會執行一個被綁定的方法,如果需要每調用一次委託就默認自動調用N個不同的被綁定的方法的話,就需要使用事件了。


調用委託,觸發事件,從調用和觸發能體會到不同之處。以下是《深入理解C#》中的:

事件不是委託實例,只是成對的add/remove方法,類似於屬性的get/set


委託是實現事件機制的一種手段。


本質論 系列。有機會都看看。基礎是讀書讀出來的,不是問出來的。


推薦閱讀:

說說C#中IList與List區別?
ADO.NET的SqlParameter(String,?Object)的構造函數第二個參數不能為0?
Entity Framework裡面 使用Code First 還是 Model First / Database First?
.NET 下的性能問題如何定位?

TAG:NET | C# |