(轉(zhuǎn)).NET面試題系列[7] - 委托與事件

委托和事件

委托在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的基石之一噪生。


image

我們看一個(gè)LINQ的例子:Where方法。


image

通過(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ì)收到信息映砖。


image

經(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市急灭,隨后出現(xiàn)的幾起案子姐浮,更是在濱河造成了極大的恐慌,老刑警劉巖葬馋,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卖鲤,死亡現(xiàn)場(chǎng)離奇詭異肾扰,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蛋逾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)集晚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人区匣,你說(shuō)我怎么就攤上這事偷拔。” “怎么了亏钩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵莲绰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我姑丑,道長(zhǎng)蛤签,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任彻坛,我火速辦了婚禮顷啼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昌屉。我一直安慰自己钙蒙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布间驮。 她就那樣靜靜地躺著躬厌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪竞帽。 梳的紋絲不亂的頭發(fā)上扛施,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音屹篓,去河邊找鬼疙渣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛堆巧,可吹牛的內(nèi)容都是我干的妄荔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼谍肤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼啦租!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起荒揣,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤篷角,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后系任,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體恳蹲,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年非凌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片航背。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖今魔,靈堂內(nèi)的尸體忽然破棺而出错森,到底是詐尸還是另有隱情涩维,我是刑警寧澤瓦阐,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布睡蟋,位于F島的核電站枷颊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏豺瘤。R本人自食惡果不足惜坐求,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一晌梨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦绰更、人聲如沸儡湾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)爹袁。三九已至呢簸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乏屯,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工辰晕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人含友。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像窘问,于是被迫代替她去往敵國(guó)和親辆童。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1把鉴,NSObject中description屬性的意義儿咱,它可以重寫(xiě)嗎?答案:每當(dāng) NSLog(@"")函數(shù)中出現(xiàn) ...
    eightzg閱讀 4,144評(píng)論 2 19
  • 概要 64學(xué)時(shí) 3.5學(xué)分 章節(jié)安排 電子商務(wù)網(wǎng)站概況 HTML5+CSS3 JavaScript Node 電子...
    阿啊阿吖丁閱讀 9,197評(píng)論 0 3
  • 山之島閱讀 167評(píng)論 0 1
  • 曾經(jīng)的鄉(xiāng)村是孩子們的樂(lè)園。放學(xué)后揭北,放下書(shū)包罐呼,相約一起侦高,來(lái)到村前的空地嫉柴,跳繩,過(guò)家家奉呛,推鐵圈计螺。熱鬧非凡,不小心碰...
    心懷夢(mèng)想1閱讀 88評(píng)論 0 0
  • 學(xué)校方面的應(yīng)用 1瞧壮、應(yīng)用在平時(shí)作業(yè) 固定比率增強(qiáng)方式能有效地應(yīng)用在班級(jí)平時(shí)作業(yè)上登馒。在班級(jí)教學(xué)上,固定比率增強(qiáng)方式...
    劉瑞瑩閱讀 208評(píng)論 0 0