委托和事件
委托在C#中具有無(wú)比重要的地位。
C#中的委托可以說(shuō)俯拾即是,從LINQ中的lambda表達(dá)式到(包括但不限于)winform,wpf中的各種事件都有著委托的身影。C#中如果沒(méi)有了事件,那絕對(duì)是一場(chǎng)災(zāi)難偶宫,令開(kāi)發(fā)者寸步難行。而委托又是事件的基礎(chǔ)环鲤,可以說(shuō)是C#的精髓纯趋,個(gè)人認(rèn)為,其地位如同指針之于C語(yǔ)言冷离。
很多開(kāi)發(fā)者并不清楚最原始版本的委托的寫(xiě)法恋腕,但是這并不妨礙他們熟練的運(yùn)用LINQ進(jìn)行查詢(xún)扭屁。對(duì)于這點(diǎn)我只能說(shuō)是微軟封裝的太好了免胃,導(dǎo)致我們竟可以完全不了解一件事物的根本蛙奖,也能正確無(wú)誤的使用。而泛型委托出現(xiàn)之后瞭空,我們也不再需要使用原始的委托聲明方式揪阿。
CLR via C#關(guān)于委托的內(nèi)容在第17章。委托不是類(lèi)型的成員之一咆畏,但事件是南捂。委托是一個(gè)密封類(lèi),可以看成是一個(gè)函數(shù)指針旧找,它可以隨情況變化為相同簽名的不同函數(shù)溺健。我們可以通過(guò)這個(gè)特點(diǎn),將不同較為相似的函數(shù)中相同的部分封裝起來(lái)钮蛛,達(dá)到復(fù)用的目的鞭缭。
回調(diào)函數(shù)
回調(diào)函數(shù)是當(dāng)一個(gè)函數(shù)運(yùn)行完之后立即運(yùn)行的另一個(gè)函數(shù),這個(gè)函數(shù)需要之前函數(shù)的運(yùn)行結(jié)果魏颓,所以不能簡(jiǎn)單的將他放在之前的函數(shù)的最后一句岭辣。回調(diào)函數(shù)在C#問(wèn)世之前就已經(jīng)存在了甸饱。在C中沦童,可以定義一個(gè)指針,指向某個(gè)函數(shù)的地址。但是這個(gè)地址不攜帶任何額外的信息搞动,比如函數(shù)期望的輸入輸出類(lèi)型,所以C中的回調(diào)函數(shù)指針不是類(lèi)型安全的渣刷。
如果類(lèi)型定義了事件成員鹦肿,那么其就可以利用事件,通知其他對(duì)象發(fā)生了特定的事情辅柴。你可能知道箩溃,也可能不知道事件什么時(shí)候會(huì)發(fā)生。例如碌嘀,Button類(lèi)提供了一個(gè)名為Click的事件涣旨,該事件只有在用戶(hù)點(diǎn)擊了位于特定位置的按鈕才會(huì)發(fā)生。想象一下如果不是使用事件股冗,而是while輪詢(xún)(每隔固定的一段時(shí)間判斷一次)的方式監(jiān)聽(tīng)用戶(hù)的點(diǎn)擊霹陡,將是多么的扯淡。事件通過(guò)委托來(lái)傳遞信息止状,可以看成是一個(gè)回調(diào)的過(guò)程烹棉,其中事件的發(fā)起者將信息通過(guò)委托傳遞給事件的處理者,后者可以看成是一個(gè)回調(diào)函數(shù)怯疤。
委托的簡(jiǎn)單調(diào)用 – 代表一個(gè)相同簽名的方法
委托可以接受一個(gè)和它的簽名相同的方法浆洗。對(duì)于簽名相同,實(shí)現(xiàn)不同的若干方法集峦,可以利用委托實(shí)現(xiàn)在不同情況下調(diào)用不同方法伏社。
使用委托分為三步:
1. 定義委托
2. 創(chuàng)建委托的一個(gè)實(shí)例,并指向一個(gè)合法的方法(其輸入和輸出和委托本身相同)
3. 同步或異步調(diào)用方法
在下面的例子中塔淤,委托指向Select方法摘昌,該方法會(huì)返回輸入list中,所有大于threshold的成員高蜂。
//1.Define
public delegate List<int> SelectDelegate(List<int> aList, int threshold);
class Program
{
static void Main(string[] args)
{
var list = new List<int>();
//Add numbers from -5 to 4
list.AddRange(Enumerable.Range(-5, 10));
//2.Initialize delegate, now delegate points to function 'Predicate'
SelectDelegate sd = Select;
//3.Invoke
list = sd.Invoke(list, 1);
//Only member > 1 are selected
Console.WriteLine("Now list has {0} members.", list.Count);
}
public static List<int> Select(List<int> aList, int threshold)
{
List<int> ret = new List<int>();
foreach (var i in aList)
{
if (i > threshold)
{
ret.Add(i);
}
}
return ret;
}
}
委托的作用 – 將方法作為方法的參數(shù)
在看完上面的例子之后第焰,可能我們?nèi)匀粫?huì)有疑惑,我們直接調(diào)用Select方法不就可以了妨马,為什么搞出來(lái)一個(gè)委托的挺举?下面就看看委托的特殊作用。我個(gè)人的理解烘跺,委托有三大重要的作用湘纵,提高擴(kuò)展性,異步調(diào)用和作為回調(diào)滤淳。
首先來(lái)看委托如何實(shí)現(xiàn)提高擴(kuò)展性梧喷。我們知道委托只能變身為和其簽名相同的函數(shù),所以我們也只能對(duì)相同簽名的函數(shù)談提高擴(kuò)展性。假設(shè)我們要寫(xiě)一個(gè)類(lèi)似計(jì)算器功能的類(lèi)铺敌,其擁有四個(gè)方法汇歹,它們的簽名都相同,都接受兩個(gè)double輸入偿凭,并輸出一個(gè)double产弹。此時(shí)常規(guī)的方法是:
public enum Operator
{
Add, Subtract, Multiply, Divide
}
public class Program
{
static void Main(string[] args)
{
double a = 1;
double b = 2;
Console.WriteLine("Result: {0}", Calculate(a, b, Operator.Divide));
}
public static double Calculate(double a, double b, Operator o)
{
switch (o)
{
case Operator.Add:
return Add(a, b);
case Operator.Subtract:
return Subtract(a, b);
case Operator.Multiply:
return Multiply(a, b);
case Operator.Divide:
return Divide(a, b);
default:
return 0;
}
}
public static double Add(double a, double b)
{
return a + b;
}
public static double Subtract(double a, double b)
{
return a - b;
}
public static double Multiply(double a, double b)
{
return a * b;
}
public static double Divide(double a, double b)
{
if (b == 0) throw new DivideByZeroException();
return a / b;
}
}
我們通過(guò)switch分支判斷輸入的運(yùn)算符號(hào),并調(diào)用對(duì)應(yīng)的方法輸出結(jié)果弯囊。不過(guò)痰哨,這樣做有一個(gè)不好的地方,就是如果日后我們?cè)僭黾悠渌倪\(yùn)算方法(具有相同的簽名)匾嘱,我們就需要修改Calculate方法斤斧,為switch增加更多的分支。我們不禁想問(wèn)霎烙,可以拿掉這個(gè)switch嗎撬讽?
如何做到去掉switch呢?我們必須要判斷運(yùn)算類(lèi)型悬垃,所以自然的想法就是將運(yùn)算類(lèi)型作為參數(shù)傳進(jìn)去锐秦,然而傳入了運(yùn)算類(lèi)型,就得通過(guò)switch判斷盗忱,思維似乎陷入了死循環(huán)酱床。但是如果我們腦洞開(kāi)大一點(diǎn)呢?如果我們通過(guò)某種方式趟佃,傳入add扇谣,subtract等方法(而不是運(yùn)算類(lèi)型),此時(shí)我們就不需要判斷了吧闲昭。
也就是說(shuō)代碼就是如下的樣子:
double a = 1;
double b = 2;
//Parse function as parameter
Console.WriteLine("Result: {0}", Calculate(a, b, Add));
Console.WriteLine("Result: {0}", Calculate(a, b, Subtract));
我們假設(shè)電腦十分聰明罐寨,看到我們傳入Add,就自動(dòng)做加法序矩,看到傳入Subtract就做減法鸯绿,最后輸出3和-1。這種情況下我們當(dāng)然不需要switch了簸淀。那么現(xiàn)在問(wèn)題來(lái)了瓶蝴,這個(gè) Calculate方法的簽名是怎么樣的?我們知道a和b都是double租幕,那么第三個(gè)參數(shù)是什么類(lèi)型舷手?什么樣的類(lèi)型既可以代表Add又可以代表Subtract?我想答案已經(jīng)呼之欲出了吧劲绪。
第三個(gè)參數(shù)當(dāng)然就是一個(gè)委托類(lèi)型男窟。首先委托本身由于要和方法簽名相同盆赤,故委托的定義只能是:
public delegate double CalculateDelegate(double a, double b);
第三個(gè)參數(shù)的簽名也只能是:
public static double Calculate(double a, double b, CalculateDelegate cd)
完整的實(shí)現(xiàn):
static void Main(string[] args)
{
double a = 1;
double b = 2;
//Parse function as parameter
Console.WriteLine("Result: {0}", Calculate(a, b, Add));
Console.WriteLine("Result: {0}", Calculate(a, b, Subtract));
}
//Invoke delegate and return corresponding result
public static double Calculate(double a, double b, CalculateDelegate cd)
{
return cd.Invoke(a, b);
}
public static double Add(double a, double b)
{
return a + b;
}
public static double Subtract(double a, double b)
{
return a - b;
}
public static double Multiply(double a, double b)
{
return a * b;
}
public static double Divide(double a, double b)
{
if (b == 0) throw new DivideByZeroException();
return a / b;
}
我們看到,我們徹底擯棄了switch這個(gè)頑疾歉眷,使得代碼的擴(kuò)展性大大增強(qiáng)了牺六。假設(shè)哪天又來(lái)了第五種運(yùn)算,我們只需要增加一個(gè)簽名相同的方法:
public static double AnotherOperation(double a, double b)
{
//TODO
}
然后調(diào)用即可:
Console.WriteLine("Result: {0}", Calculate(a, b, AnotherOperation));
擴(kuò)展閱讀:函數(shù)式編程
許多人初學(xué)委托無(wú)法理解的一個(gè)重要原因是汗捡,總是把變量和方法看成不同的東西淑际。方法必須輸入若干變量,然后對(duì)它們進(jìn)行操作凉唐,最后輸出結(jié)果。但是實(shí)際上霍骄,方法本身也可以看成是一種特殊類(lèi)型的變量台囱。
相同簽名的方法具有相同的類(lèi)型,在C#****中读整,這個(gè)特殊的類(lèi)型有一個(gè)名字簿训,就叫做委托。如果說(shuō)double****代表了(幾乎)所有的小數(shù)米间,那么輸入為double****强品,輸出為double****的委托,代表了所有簽名為****輸入為double****屈糊,輸出為double****的方法的榛。所以,方法是變量的一種形式逻锐,方法既然可以接受變量夫晌,當(dāng)然也可以接受另一個(gè)方法。
函數(shù)式編程是繼面向?qū)ο笾笪磥?lái)的發(fā)展方向之一昧诱。簡(jiǎn)單來(lái)說(shuō)晓淀,就是在函數(shù)式編程的環(huán)境下,你是在寫(xiě)函數(shù)盏档,將一個(gè)集合通過(guò)函數(shù)映射到另一個(gè)集合凶掰。例如f(x)=x+1就是一個(gè)這樣的映射,它將輸入集合中所有的元素都加1蜈亩,并將結(jié)果作為輸出集合懦窘。由于你所有的函數(shù)都是吃進(jìn)去集合,吐出來(lái)集合稚配,所以你當(dāng)然可以pipeline式的進(jìn)行調(diào)用奶赠,從而實(shí)現(xiàn)一連串操作,既簡(jiǎn)單又優(yōu)雅药有。
許多語(yǔ)言毅戈,例如javascript苹丸,C#都有函數(shù)式編程的性質(zhì)。在以后的文章中苇经,我們可以看到LINQ有很多函數(shù)式編程的特點(diǎn):pipeline赘理,currying等。有關(guān)函數(shù)式編程的內(nèi)容可以參考:http://coolshell.cn/articles/10822.html以及http://www.ruanyifeng.com/blog/2012/04/functional_programming.html
委托的作用 – 異步調(diào)用和作為回調(diào)函數(shù)扇单,委托的異步編程模型(APM)
通過(guò)委托的BeginInvoke方法可以實(shí)現(xiàn)異步調(diào)用商模。由于委托可以代表任意一類(lèi)方法,所以你可以通過(guò)委托異步調(diào)用任何方法蜘澜。對(duì)于各種各樣的異步實(shí)現(xiàn)方式施流,委托是其中最早出現(xiàn)的一個(gè),在C#1.0就出現(xiàn)了鄙信,和Thread的歷史一樣長(zhǎng)瞪醋。
異步調(diào)用有幾個(gè)關(guān)鍵點(diǎn)需要注意:
- 如何取消一個(gè)異步操作?
- 如何獲得異步調(diào)用的結(jié)果装诡?
- 如何實(shí)現(xiàn)一個(gè)回調(diào)函數(shù)银受,當(dāng)異步調(diào)用結(jié)束時(shí)立刻執(zhí)行?
- 對(duì)于各種異步實(shí)現(xiàn)方式鸦采,都要留心上面的幾個(gè)問(wèn)題宾巍。異步是一個(gè)非常巨大的話(huà)題,我現(xiàn)在也沒(méi)有學(xué)到熟練的地步渔伯。
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的異步調(diào)用首先我們需要一個(gè)比較耗時(shí)的任務(wù)顶霞。在這里我打算通過(guò)某種算法,判斷某個(gè)大數(shù)是否為質(zhì)數(shù)锣吼。
public static bool IsPrimeNumber(long number)
{
if (number == 1) throw new Exception("1 is neither prime nor composite number");
if (number % 2 == 0) return false;
//int sqrt = (int) Math.Floor(Math.Sqrt(number));
for (int i = 2; i < number; i++)
{
if (number%i == 0) return false;
}
return true;
}
上面的算法中我故意撤去了計(jì)算平方根這步确丢,使得算法的性能大大變差了,達(dá)到耗時(shí)的目的吐限。為了拖慢時(shí)間鲜侥,我們找一個(gè)巨大的質(zhì)數(shù)1073676287,這樣诸典,整個(gè)for循環(huán)要全部運(yùn)行一次才會(huì)結(jié)束描函,而不會(huì)提早break。
為了異步調(diào)用狐粱,要先聲明一個(gè)和方法簽名相同的委托才行:
public delegate void ClongBigFileDelegate(string path);
然后舀寓,我們就在主程序中簡(jiǎn)單的異步調(diào)用。我們發(fā)現(xiàn)BeginInvoke的參數(shù)數(shù)目比Invoke多了兩個(gè)肌蜻,不過(guò)現(xiàn)在我們先不管它互墓,將它們都設(shè)置為null:
IsPrimeNumberDelegate d = new IsPrimeNumberDelegate(IsPrimeNumber);
d.BeginInvoke(1073676287, null, null);
Console.WriteLine("I am doing something else.");
Console.ReadKey();
這樣雖然實(shí)現(xiàn)了異步調(diào)用(主程序會(huì)馬上離開(kāi)BeginInvoke打印下面的話(huà)),但也有很多問(wèn)題:
- 如果不加上Console.ReadKey蒋搜,主程序會(huì)直接關(guān)閉篡撵,因?yàn)槲ㄒ坏那芭_(tái)線(xiàn)程結(jié)束運(yùn)行了(winform則不存在這個(gè)問(wèn)題判莉,除非你終止程序,前臺(tái)線(xiàn)程永遠(yuǎn)不會(huì)結(jié)束運(yùn)行)
- 異步調(diào)用具體什么時(shí)候結(jié)束工作不知道育谬∪眩可能很快就結(jié)束了,可能剛進(jìn)行了5%膛檀,總之就是看不出來(lái)(但如果你手賤敲了任意一個(gè)鍵锰镀,程序立馬結(jié)束),也不能實(shí)現(xiàn)“當(dāng)異步調(diào)用結(jié)束之后咖刃,主程序繼續(xù)運(yùn)行某些代碼”
- 算了半天泳炉,不知道結(jié)果...
你可能也想到了,BeginInvoke后兩個(gè)神秘的輸入?yún)?shù)可能能幫你解決上面的問(wèn)題嚎杨。
通過(guò)EndInvoke獲得異步委托的執(zhí)行結(jié)果
我們可以通過(guò)EndInvoke獲得委托標(biāo)的函數(shù)的返回值:
IAsyncResult ia = d.BeginInvoke(1073676287, null, null);
Console.WriteLine("I am doing something else.");
var ret = d.EndInvoke(ia);
Console.WriteLine("Calculation finished. Number is prime number : {0}", ret == true ? "Yes" : "No");
Console.ReadKey();
這解決了第一個(gè)問(wèn)題和第三個(gè)問(wèn)題』ǘ欤現(xiàn)在你再運(yùn)行程序,程序會(huì)阻塞在EndInvoke磕潮,你手賤敲了任意一個(gè)鍵翠胰,程序也不會(huì)結(jié)束容贝。另外自脯,我們還獲得了異步委托的結(jié)果,即該大數(shù)是質(zhì)數(shù)斤富。
但這個(gè)解決方法又衍生出了一個(gè)新的問(wèn)題:即程序會(huì)阻塞在EndInvoke膏潮,如果這是一個(gè)GUI程序,主線(xiàn)程將會(huì)卡死满力,給用戶(hù)帶來(lái)不好的體驗(yàn)焕参。如何解決這個(gè)問(wèn)題?
通過(guò)回調(diào)函數(shù)獲得異步委托的執(zhí)行結(jié)果
回調(diào)函數(shù)的用處是當(dāng)委托完成時(shí)油额,可以主動(dòng)通知主線(xiàn)程自己已經(jīng)完成叠纷。我們可以在BeginInvoke中定義回調(diào)函數(shù),這將會(huì)在委托完成時(shí)自動(dòng)執(zhí)行潦嘶。
回調(diào)函數(shù)的類(lèi)型是AsyncCallback涩嚣,其也是一個(gè)委托,它的簽名:傳入?yún)?shù)必須是IAsyncResult掂僵,而且沒(méi)有返回值航厚。所以我們的回調(diào)函數(shù)必須長(zhǎng)成這樣子:
public static void IsPrimeNumberCallback(IAsyncResult iar)
{
}
在主函數(shù)中加入回調(diào)函數(shù):
AsyncCallback acb = new AsyncCallback(IsPrimeNumberCallback);
d.BeginInvoke(1073676287, acb, null);
IAsyncResult中并不包括委托的返回值。利用AsyncCallback可以被轉(zhuǎn)換成AsyncResult類(lèi)型的特點(diǎn)锰蓬,我們可以利用AsyncResult中的AsyncDelegate“克隆”一個(gè)當(dāng)前正在運(yùn)行的委托幔睬,然后調(diào)用克隆委托的EndInvoke。因?yàn)檫@時(shí)委托已經(jīng)執(zhí)行完了所以EndInvoke不會(huì)阻塞:
public static void IsPrimeNumberCallback(IAsyncResult iar)
{
AsyncResult ar = (AsyncResult) iar;
var anotherDelegate = (IsPrimeNumberDelegate) iar.AsyncDelegate;
var ret = anotherDelegate.EndInvoke(iar);
Console.WriteLine("Calculation finished, Number is prime number : {0}", ret == true ? "Yes" : "No");
}
看到這里讀者大概要感慨了芹扭,使用委托異步調(diào)用獲得結(jié)果怎么這么復(fù)雜麻顶。確實(shí)是比較復(fù)雜赦抖,所以之后微軟就在后續(xù)版本的C#中加入了任務(wù)這個(gè)工具,它大大簡(jiǎn)化了異步調(diào)用的編寫(xiě)方式澈蚌。
總結(jié)
使用委托的異步編程模型(APM):
- 通過(guò)建立一個(gè)委托和使用BeginInvoke調(diào)用委托來(lái)實(shí)現(xiàn)異步摹芙,通過(guò)EndInvoke來(lái)獲得結(jié)果,但要注意的是宛瞄,EndInvoke會(huì)令主線(xiàn)程進(jìn)入阻塞狀態(tài)浮禾,卡死主線(xiàn)程,所以我們通常使用回調(diào)函數(shù)
- BeginInvoke方法擁有委托全部的輸入份汗,以及額外的兩個(gè)輸入
- 第一個(gè)輸入為委托的回調(diào)函數(shù)盈电,它是AsyncCallback類(lèi)型,這個(gè)類(lèi)型是一個(gè)委托杯活,其輸入必須是IAsyncResult類(lèi)型匆帚,且沒(méi)有返回值,如果需要獲得返回值旁钧,需要在回調(diào)函數(shù)中吸重,再次呼叫EndInvoke,并傳入IAsyncResult
- 委托的回調(diào)函數(shù)在次線(xiàn)程任務(wù)結(jié)束時(shí)自動(dòng)執(zhí)行歪今,并替代EndInvoke
- 第二個(gè)輸入為object類(lèi)型嚎幸,允許你為異步線(xiàn)程傳入自定義數(shù)據(jù)
- 因?yàn)槭褂梦械漠惒秸{(diào)用本質(zhì)上也是通過(guò)線(xiàn)程來(lái)實(shí)現(xiàn)異步編程的,所以也可以使用同Threading相同的取消方法寄猩,但這實(shí)在是太過(guò)麻煩(你需要手寫(xiě)一個(gè)CancellationToken嫉晶,這部分到說(shuō)到線(xiàn)程的時(shí)候再說(shuō))
- 關(guān)于進(jìn)度條的問(wèn)題,要等到更高級(jí)的BackgroundWorker來(lái)解決
- 我們看到獲取異步結(jié)果這一步還是比較麻煩田篇,所以在任務(wù)和BackgroundWorker等大殺器出現(xiàn)之后替废,這個(gè)模型就基本不會(huì)使用了
多路廣播
委托的本質(zhì)是一個(gè)密封類(lèi)。這個(gè)類(lèi)繼承自System.MultiDelegate泊柬,其再繼承自System.Delegate椎镣。System.MulticastDelegate類(lèi)中有一個(gè)重要字段_invocationList,它令委托可以?huà)旖佣嘤谝粋€(gè)函數(shù)(即一個(gè)函數(shù)List)兽赁。它維護(hù)一個(gè)Invocation List(委托鏈)状答。你可以為這個(gè)鏈自由的添加或刪除Handler函數(shù)。一個(gè)委托鏈可以沒(méi)有函數(shù)闸氮。
由于委托可以代表一類(lèi)函數(shù)剪况,你可以隨心所欲的為委托鏈綁定合法的函數(shù)。此時(shí)如果執(zhí)行委托蒲跨,將會(huì)順序的執(zhí)行委托鏈上所有的函數(shù)译断。如果某個(gè)函數(shù)出現(xiàn)了異常,則其后所有的函數(shù)都不會(huì)執(zhí)行或悲。
如果你的委托的委托鏈含有很多委托的話(huà)孙咪,你只會(huì)收到最后一個(gè)含有返回值的委托的返回值堪唐。假如你的委托是有輸出值的,而且你想得到委托鏈上所有方法的輸出值翎蹈,你只能通過(guò)GetInvocationList方法得到委托鏈上的所有方法淮菠,然后一一執(zhí)行。
委托的本質(zhì)
本節(jié)大部分都是概念荤堪,如果你正在準(zhǔn)備面試合陵,而且已經(jīng)沒(méi)有多少時(shí)間了,可以考慮將它們背下來(lái)澄阳。
委托的本質(zhì)是一個(gè)密封類(lèi)拥知。這個(gè)類(lèi)繼承自System.MultiDelegate,其再繼承自System.Delegate。這個(gè)密封類(lèi)包括三個(gè)核心函數(shù),Invoke方法賦予其同步訪(fǎng)問(wèn)的能力抵蚊,BeginInvoke,EndInvoke賦予其異步訪(fǎng)問(wèn)的能力逗柴。例如public delegate int ADelegate(out z,int x,int y)的三個(gè)核心函數(shù):
1.int Invoke (out z,int x猜欺,int y)
2.IAsyncResult BeginInvoke (out z,int x烁设,int y替梨,AsyncCallback cb钓试,object ob)
3.int EndInvoke (out z装黑,IAsyncResult result)
4.Invoke方法的參數(shù)和返回值同委托本身相同,BeginInvoke的返回值總是IAsyncResult弓熏,輸入則除了委托本身的輸入之外還包括了AsyncCallback(回調(diào)函數(shù))和一個(gè)object恋谭。EndInvoke的輸入總是IAsyncResult,加上委托中的out和ref(如果有的話(huà))類(lèi)型的輸入挽鞠,輸出類(lèi)型則是委托的輸出類(lèi)型疚颊。在事件中,委托是事件的發(fā)起者sender將EventArgs傳遞給處理者的管道信认。所以委托是一個(gè)密封類(lèi)材义,沒(méi)有繼承的意義。
委托可以看成是函數(shù)指針嫁赏,它接受與其簽名相同的任何函數(shù)其掂。委托允許你把方法作為參數(shù)。
相比C的函數(shù)指針潦蝇,C#的委托是類(lèi)型安全的款熬,可以方便的獲得回調(diào)函數(shù)的返回值深寥,并且可以通過(guò)委托鏈支持多路廣播。
EventHandler委托類(lèi)型是.NET自帶的一個(gè)委托贤牛。其不返回任何值惋鹅,輸入為object類(lèi)型的sender和EventArgs類(lèi)型的e。如果你想返回自定義的數(shù)據(jù)殉簸,你必須繼承EventArgs類(lèi)型闰集。這個(gè)委托十分適合處理不需要返回值的事件,例如點(diǎn)擊按鈕事件般卑。
System.MulticastDelegate類(lèi)中有一個(gè)重要字段_invocationList返十,它令委托可以?huà)旖佣嘤谝粋€(gè)函數(shù)(即一個(gè)函數(shù)List)。它維護(hù)一個(gè)Invocation List(委托鏈)椭微。你可以為這個(gè)鏈自由的添加或刪除Handler函數(shù)洞坑。一個(gè)委托鏈可以沒(méi)有函數(shù)。添加或刪除實(shí)質(zhì)上是調(diào)用了Delegate.Combine / Delegate.Remove蝇率。
當(dāng)你為一個(gè)沒(méi)有任何函數(shù)的委托鏈刪除方法時(shí)迟杂,不會(huì)發(fā)生異常,僅僅是沒(méi)有產(chǎn)生任何效果本慕。
假設(shè)委托可以返回值排拷,那么如果你的委托的委托鏈含有很多委托的話(huà),你只會(huì)收到最后一個(gè)委托的返回值锅尘。
如果在委托鏈中的某個(gè)操作出現(xiàn)了異常监氢,則其后任何的操作都不會(huì)執(zhí)行。如果你想要讓所有委托掛接的函數(shù)至少執(zhí)行一次藤违,你需要使用GetInvocationList方法浪腐,從委托鏈中獲得方法,然后手動(dòng)執(zhí)行他們顿乒。
泛型委托
泛型委托Action和Func是兩個(gè)委托议街,Action<T>接受一個(gè)T類(lèi)型的輸入,沒(méi)有輸出璧榄。Func則有一個(gè)輸出特漩,16個(gè)重載分別對(duì)應(yīng)1-16個(gè)T類(lèi)型的輸入(這使得它更像數(shù)學(xué)中函數(shù)的概念,故名Func)骨杂。Func委托的最后一個(gè)參數(shù)是返回值的類(lèi)型涂身,前面的參數(shù)都是輸入值的類(lèi)型。
在它們出現(xiàn)之后搓蚪,你就不需要使用delegate關(guān)鍵字聲明委托了(即你可以忘記它了)蛤售,你可以使用泛型委托代替之。
static void Main(string[] args)
{
Action<int, int> a = new Action<int悍抑, int>(add);
a(1, 2);
//Func委托的最后一個(gè)參數(shù)是返回值的類(lèi)型
Func<int搜骡, int拂盯, int> b = new Func<int, int记靡, int>(add2);
Console.WriteLine(b(1摸吠, 2));
Console.ReadLine();
}
//這個(gè)EventHandler不返回值
public static void add(int a啼止, int b)
{
Console.WriteLine(a + b);
}
//這個(gè)EventHandler返回一個(gè)整數(shù)
public static int add2(int a献烦, int b)
{
return a+b;
}
我們可以看到使用Action對(duì)代碼的簡(jiǎn)化吏夯。我們不用再自定義一個(gè)委托,并為其取名了即横。這兩個(gè)泛型委托構(gòu)成了LINQ的基石之一噪生。
我們看一個(gè)LINQ的例子:Where方法。
通過(guò)閱讀VS的解釋?zhuān)覀兛梢垣@得以下信息:
1.Where是IEnumerable<T>的一個(gè)擴(kuò)展方法
2.這個(gè)方法的輸入是一個(gè)Func<T令境,bool>杠园,形如Func<T顾瞪,bool>的泛型委托又有別名Predicate舔庶,因其是返回一個(gè)布爾型的輸出,故有判斷之意陈醒。
泛型委托使用一例
下面這個(gè)問(wèn)題是某著名公司的一個(gè)面試題目惕橙。其主要的問(wèn)題就是,如何對(duì)兩個(gè)對(duì)象比較大小钉跷,這里面的對(duì)象可以是任意的東西弥鹦。這個(gè)題目主要考察的是如何使用泛型和委托結(jié)合,實(shí)現(xiàn)代碼復(fù)用的目的。
假設(shè)我們有若干個(gè)表示形狀的結(jié)構(gòu)體彬坏,我們要比較它們的大小朦促。
public struct Rectangle
{
public double Length { get; set; }
public double Width { get; set; }
//By calling this() to initialize all valuetype members
public Rectangle(double l, double w) : this()
{
Length = l;
Width = w;
}
}
public struct Circle
{
public double Radius { get; set; }
public Circle(double r) : this()
{
Radius = r;
}
}
我們規(guī)定誰(shuí)面積大就算誰(shuí)大,此時(shí)栓始,因?yàn)榻Y(jié)構(gòu)體不能比較大小务冕,只能比較是否相等,我們就需要自己制定一個(gè)規(guī)則幻赚。對(duì)不同的形狀禀忆,求面積的公式也不一樣:
public static int CompareRectangle(Rectangle r1, Rectangle r2)
{
double r1Area = r1.Length*r1.Width;
double r2Area = r2.Length*r2.Width;
if (r1Area > r2Area) return 1;
if (r1Area < r2Area) return -1;
return 0;
}
public static int CompareCircle(Circle c1, Circle c2)
{
if (c1.Radius > c2.Radius) return 1;
if (c1.Radius < c2.Radius) return -1;
return 0;
}
當(dāng)然,在比較大小的時(shí)候落恼,可以直接調(diào)用這些函數(shù)箩退。但如果這么做,你將再次陷入“委托的作用-將方法作為方法的參數(shù)”一節(jié)中的switch泥潭佳谦。注意到這些函數(shù)的簽名都相同戴涝,我們現(xiàn)在已經(jīng)熟悉委托了,當(dāng)然就可以用委托來(lái)簡(jiǎn)化代碼钻蔑。
我們可以把規(guī)則看作一個(gè)函數(shù)喊括,其輸入為兩個(gè)同類(lèi)型的對(duì)象,輸出一個(gè)整數(shù)矢棚,當(dāng)?shù)匾粋€(gè)對(duì)象較大時(shí)輸出1郑什,相等輸出0,第二個(gè)對(duì)象較大輸出-1蒲肋。那么蘑拯,這個(gè)規(guī)則函數(shù)的簽名應(yīng)當(dāng)為:
Func<T, T, int>
它可以變身為任意類(lèi)型的比較函數(shù)。我們?cè)谕獠吭侔b一下兜粘,將這個(gè)規(guī)則傳入進(jìn)去申窘。那么這個(gè)外部包裝函數(shù)的簽名應(yīng)當(dāng)為:
public static void Compare<T>(T o1, T o2, Func<T, T, int> rule)
{
}
當(dāng)然這里的返回值也可以是int。由于是演示的緣故孔轴,我就簡(jiǎn)單的打印一些信息:
public static void Compare<T>(T o1, T o2, Func<T, T, int> rule)
{
var ret = rule.Invoke(o1, o2);
if (ret == 1) Console.WriteLine("First object is bigger.");
if (ret == -1) Console.WriteLine("Second object is bigger.");
if (ret == 0) Console.WriteLine("They are the same.");
}
主程序調(diào)用:
static void Main(string[] args)
{
var r1 = new Rectangle(1, 6);
var r2 = new Rectangle(2, 4);
Compare(r1, r2, CompareRectangle);
var c1 = new Circle(3);
var c2 = new Circle(2);
Compare(c1, c2, CompareCircle);
Console.ReadKey();
}
我們可以看到剃法,對(duì)不同類(lèi)型都有著統(tǒng)一的比較大小的方式÷酚ィ可以參考:http://www.cnblogs.com/onepiece_wang/archive/2012/11/28/2793530.html
什么是事件贷洲?
簡(jiǎn)單的看,事件的定義就是通知(給訂閱者)晋柱。事件由三部分組成:事件的觸發(fā)者(sender)优构,事件的處理者(Event Handler,一個(gè)和委托類(lèi)型相同的函數(shù))和事件的數(shù)據(jù)傳送通道delegate雁竞。delegate負(fù)責(zé)傳輸事件的觸發(fā)者對(duì)象sender和自定義的數(shù)據(jù)EventArgs钦椭。要實(shí)現(xiàn)事件,必須實(shí)現(xiàn)中間的委托(的標(biāo)的函數(shù)),并為事件提供一個(gè)處理者彪腔。處理者函數(shù)的簽名和委托必須相同侥锦。
所以,事件必須基于一個(gè)委托德挣。
使用事件的步驟:
- 聲明委托(指出當(dāng)事件發(fā)生時(shí)要執(zhí)行的方法的方法類(lèi)型)捎拯。委托要傳遞的數(shù)據(jù)可能是自定義類(lèi)型的
- 聲明一個(gè)事件處理者(一個(gè)方法),其簽名和委托簽名相同
- 聲明一個(gè)事件(這需要第一步的委托)
- 為事件+=事件處理者(委托對(duì)象即是訂閱者/消費(fèi)者)
-
在事件符合條件之后盲厌,調(diào)用事件
image
委托和事件有何關(guān)系署照?
委托是事件傳輸消息的管道。事件必須基于一個(gè)委托吗浩。下圖中小女孩是事件的發(fā)起者(擁有者)建芙,她通過(guò)委托(即圖上的“電話(huà)線(xiàn)”)傳遞若干消息給她的爸爸(事件的處理者/訂閱者)。和委托一樣懂扼,事件可以有多個(gè)訂閱者禁荸,這也是多路廣播的一個(gè)體現(xiàn)。
可以借助事件實(shí)現(xiàn)觀察者模式阀湿。觀察者模式刻畫(huà)了一個(gè)一對(duì)多的依賴(lài)關(guān)系赶熟,其中,當(dāng)一對(duì)多中的“一”發(fā)生變化時(shí)陷嘴,“多”的那頭會(huì)收到信息映砖。
經(jīng)典例子:this.button1.Click += new System.EventHandler(this.StartButton_Click);
- Click是一個(gè)事件,它的定義為public event EventHandler Click灾挨,它基于的委托類(lèi)型是EventHandler類(lèi)型邑退。
- Click事件掛接了一個(gè)新的委托,委托傳遞object類(lèi)型的sender和EventArgs類(lèi)型的e給事件的處理者StartButton_Click劳澄。StartButton_Click是一個(gè)和EventHandler委托類(lèi)型簽名相同的函數(shù)地技。
- EventHandler是.NET自帶的一個(gè)委托。其不返回任何值秒拔,輸入為object類(lèi)型的sender和EventArgs類(lèi)型的e莫矗。EventArgs類(lèi)型本身沒(méi)有任何成員,如果你想傳遞自定義的數(shù)據(jù)砂缩,你必須繼承EventArgs類(lèi)型作谚。
使用事件
使用事件需要至少一個(gè)訂閱者。訂閱者需要一個(gè)事件處理函數(shù)梯轻,該處理函數(shù)通常要具備兩個(gè)參數(shù):輸入為object類(lèi)型的sender和一個(gè)繼承了EventArgs類(lèi)型的e(有時(shí)候第一個(gè)參數(shù)是不必要的)食磕。你需要繼承EventArgs類(lèi)型來(lái)傳遞自定義數(shù)據(jù)。
public class Subscriber
{
public string Name { get; set; }
public Subscriber(string name)
{
Name = name;
}
public void ReceiveMessage(object sender, MessageArgs e)
{
Console.WriteLine("I am {0} and I know {1}!", Name, e.Message);
}
}
public class MessageArgs : EventArgs
{
public string Message { get; set; }
}
當(dāng)有訂閱者訂閱事件之后喳挑,Invoke事件會(huì)順序激發(fā)所有訂閱者的事件處理函數(shù)。其激發(fā)順序視訂閱順序而定。
首先要定義委托和事件伊诵。委托的命名慣例是以Handler結(jié)尾:
//1. Base delegate
public delegate void SendMessageHandler(object sender, MessageArgs e);
//2. Event based on the delegate
public static event SendMessageHandler SendMessage;
事件的執(zhí)行演示:
static void Main(string[] args)
{
//Subscribers
Subscriber s1 = new Subscriber("Adam");
Subscriber s2 = new Subscriber("Betty");
Subscriber s3 = new Subscriber("Clara");
//Subscribe
SendMessage += s1.ReceiveMessage;
SendMessage += s2.ReceiveMessage;
SendMessage += s3.ReceiveMessage;
//Simulate a message transfer
Console.WriteLine("Simulate initializing...");
Thread.Sleep(new Random(1).Next(0, 1000));
var data = new MessageArgs {Message = "Class begins"};
if (SendMessage != null) SendMessage(null, data);
//Unsubscribe
SendMessage -= s1.ReceiveMessage;
Thread.Sleep(new Random(1).Next(0, 1000));
data.Message = "Calling from main function";
if (SendMessage != null) SendMessage(null, data);
Console.WriteLine("Class is over!");
Console.ReadKey();
}
事件的本質(zhì)
如果你查看事件屬性的對(duì)應(yīng)IL单绑,你會(huì)發(fā)現(xiàn)它實(shí)質(zhì)上是一個(gè)私有的字段,包含兩個(gè)方法add_[事件名]和remove_[事件名]曹宴。
事件是私有的搂橙,它和委托的關(guān)系類(lèi)似屬性和字段的關(guān)系。它封裝了委托笛坦,用戶(hù)只能通過(guò)add_[事件名]和remove_[事件名](也就是+=和-=)進(jìn)行訪(fǎng)問(wèn)区转。
如果訂閱事件的多個(gè)訂閱者在事件觸發(fā)時(shí),有一個(gè)訂閱者的事件處理函數(shù)引發(fā)了異常版扩,則它將會(huì)影響后面的訂閱者废离,后面的訂閱者的事件處理函數(shù)不會(huì)運(yùn)行。
如果你希望事件只能被一個(gè)客戶(hù)訂閱礁芦,則你可以將事件本身私有蜻韭,然后暴露一個(gè)注冊(cè)的方法。在注冊(cè)時(shí)柿扣,直接使用等號(hào)而不是+=就可以了肖方,后來(lái)的客戶(hù)會(huì)將前面的客戶(hù)覆蓋掉。
委托的協(xié)變和逆變
協(xié)變和逆變實(shí)際上是屬于泛型的語(yǔ)法特性未状,由于有泛型委托的存在俯画,故委托也具備這個(gè)特性。我將在討論泛型的時(shí)候再深入討論這個(gè)特性司草。
經(jīng)典文章活翩,參考資料
有關(guān)委托和事件的文章多如牛毛。熟悉了委托和事件翻伺,將會(huì)對(duì)你理解linq有很大的幫助材泄。
1. 張子陽(yáng)的經(jīng)典例子: http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html
可以自行編寫(xiě)一個(gè)熱水器的例子,測(cè)試自己是否掌握了基本的事件用法吨岭。
http://www.cnblogs.com/JimmyZhang/archive/2008/08/22/1274342.html 這是續(xù)篇拉宗。
2. 委托本質(zhì)論,不過(guò)說(shuō)的比較簡(jiǎn)單辣辫。這個(gè)水平也基本可以應(yīng)付面試了(很少有人問(wèn)這么深入)旦事,更難更全面的解釋可以參考clr via c#:http://www.cnblogs.com/zhili/archive/2012/10/25/DeepDelegate.html
3. 一個(gè)生動(dòng)的事件例子:http://www.cnblogs.com/yinqixin/p/5056307.html
4. 常見(jiàn)委托面試題目:http://www.cnblogs.com/jackson0714/p/5111347.html