天天看點

委托和事件

委托和事件

委托在C#中具有無比重要的地位。

C#中的委托可以說俯拾即是,從LINQ中的lambda表達式到(包括但不限于)winform,wpf中的各種事件都有着委托的身影。C#中如果沒有了事件,那絕對是一場災難,令開發者寸步難行。而委托又是事件的基礎,可以說是C#的精髓,個人認為,其地位如同指針之于C語言。

很多開發者并不清楚最原始版本的委托的寫法,但是這并不妨礙他們熟練的運用LINQ進行查詢。對于這點我隻能說是微軟封裝的太好了,導緻我們竟可以完全不了解一件事物的根本,也能正确無誤的使用。而泛型委托出現之後,我們也不再需要使用原始的委托聲明方式。

CLR via C#關于委托的内容在第17章。委托不是類型的成員之一,但事件是。委托是一個密封類,可以看成是一個函數指針,它可以随情況變化為相同簽名的不同函數。我們可以通過這個特點,将不同較為相似的函數中相同的部分封裝起來,達到複用的目的。

回調函數

回調函數是當一個函數運作完之後立即運作的另一個函數,這個函數需要之前函數的運作結果,是以不能簡單的将他放在之前的函數的最後一句。回調函數在C#問世之前就已經存在了。在C中,可以定義一個指針,指向某個函數的位址。但是這個位址不攜帶任何額外的資訊,比如函數期望的輸入輸出類型,是以C中的回調函數指針不是類型安全的。

如果類型定義了事件成員,那麼其就可以利用事件,通知其他對象發生了特定的事情。你可能知道,也可能不知道事件什麼時候會發生。例如,Button類提供了一個名為Click的事件,該事件隻有在使用者點選了位于特定位置的按鈕才會發生。想象一下如果不是使用事件,而是while輪詢(每隔固定的一段時間判斷一次)的方式監聽使用者的點選,将是多麼的扯淡。事件通過委托來傳遞資訊,可以看成是一個回調的過程,其中事件的發起者将資訊通過委托傳遞給事件的處理者,後者可以看成是一個回調函數。

委托的簡單調用 – 代表一個相同簽名的方法

委托可以接受一個和它的簽名相同的方法。對于簽名相同,實作不同的若幹方法,可以利用委托實作在不同情況下調用不同方法。

使用委托分為三步:

1. 定義委托

2. 建立委托的一個執行個體,并指向一個合法的方法(其輸入和輸出和委托本身相同)

3. 同步或異步調用方法

在下面的例子中,委托指向Select方法,該方法會傳回輸入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;
        }
    }      
委托和事件

委托的作用 – 将方法作為方法的參數

在看完上面的例子之後,可能我們仍然會有疑惑,我們直接調用Select方法不就可以了,為什麼搞出來一個委托的?下面就看看委托的特殊作用。我個人的了解,委托有三大重要的作用,提高擴充性,異步調用和作為回調。

首先來看委托如何實作提高擴充性。我們知道委托隻能變身為和其簽名相同的函數,是以我們也隻能對相同簽名的函數談提高擴充性。假設我們要寫一個類似電腦功能的類,其擁有四個方法,它們的簽名都相同,都接受兩個double輸入,并輸出一個double。此時正常的方法是:

委托和事件
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;
        }
    }      
委托和事件

我們通過switch分支判斷輸入的運算符号,并調用對應的方法輸出結果。不過,這樣做有一個不好的地方,就是如果日後我們再增加其他的運算方法(具有相同的簽名),我們就需要修改Calculate方法,為switch增加更多的分支。我們不禁想問,可以拿掉這個switch嗎?

如何做到去掉switch呢?我們必須要判斷運算類型,是以自然的想法就是将運算類型作為參數傳進去,然而傳入了運算類型,就得通過switch判斷,思維似乎陷入了死循環。但是如果我們腦洞開大一點呢?如果我們通過某種方式,傳入add,subtract等方法(而不是運算類型),此時我們就不需要判斷了吧。

也就是說代碼就是如下的樣子:

委托和事件
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));      
委托和事件

我們假設電腦十分聰明,看到我們傳入Add,就自動做加法,看到傳入Subtract就做減法,最後輸出3和-1。這種情況下我們當然不需要switch了。那麼現在問題來了,這個 Calculate方法的簽名是怎麼樣的?我們知道a和b都是double,那麼第三個參數是什麼類型?什麼樣的類型既可以代表Add又可以代表Subtract?我想答案已經呼之欲出了吧。

第三個參數當然就是一個委托類型。首先委托本身由于要和方法簽名相同,故委托的定義隻能是:

public delegate double CalculateDelegate(double a, double b);      

第三個參數的簽名也隻能是:

public static double Calculate(double a, double b, CalculateDelegate cd)      

完整的實作:

委托和事件
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這個頑疾,使得代碼的擴充性大大增強了。假設哪天又來了第五種運算,我們隻需要增加一個簽名相同的方法:

public static double AnotherOperation(double a, double b)
        {
            //TODO
        }      

然後調用即可:

Console.WriteLine("Result: {0}", Calculate(a, b, AnotherOperation));      

擴充閱讀:函數式程式設計

許多人初學委托無法了解的一個重要原因是,總是把變量和方法看成不同的東西。方法必須輸入若幹變量,然後對它們進行操作,最後輸出結果。但是實際上,方法本身也可以看成是一種特殊類型的變量。

相同簽名的方法具有相同的類型,在C#中,這個特殊的類型有一個名字,就叫做委托。如果說double代表了(幾乎)所有的小數,那麼輸入為double,輸出為double的委托,代表了所有簽名為輸入為double,輸出為double的方法。是以,方法是變量的一種形式,方法既然可以接受變量,當然也可以接受另一個方法。

函數式程式設計是繼面向對象之後未來的發展方向之一。簡單來說,就是在函數式程式設計的環境下,你是在寫函數,将一個集合通過函數映射到另一個集合。例如f(x)=x+1就是一個這樣的映射,它将輸入集合中所有的元素都加1,并将結果作為輸出集合。由于你所有的函數都是吃進去集合,吐出來集合,是以你當然可以pipeline式的進行調用,進而實作一連串操作,既簡單又優雅。

許多語言,例如javascript,C#都有函數式程式設計的性質。在以後的文章中,我們可以看到LINQ有很多函數式程式設計的特點:pipeline,currying等。有關函數式程式設計的内容可以參考:http://coolshell.cn/articles/10822.html以及http://www.ruanyifeng.com/blog/2012/04/functional_programming.html 

委托的作用 – 異步調用和作為回調函數,委托的異步程式設計模型(APM)

通過委托的BeginInvoke方法可以實作異步調用。由于委托可以代表任意一類方法,是以你可以通過委托異步調用任何方法。對于各種各樣的異步實作方式,委托是其中最早出現的一個,在C#1.0就出現了,和Thread的曆史一樣長。

異步調用有幾個關鍵點需要注意:

  • 如何取消一個異步操作?
  • 如何獲得異步調用的結果?
  • 如何實作一個回調函數,當異步調用結束時立刻執行?

對于各種異步實作方式,都要留心上面的幾個問題。異步是一個非常巨大的話題,我現在也沒有學到熟練的地步。

實作一個簡單的異步調用首先我們需要一個比較耗時的任務。在這裡我打算通過某種算法,判斷某個大數是否為質數。

委托和事件
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;
        }      
委托和事件

上面的算法中我故意撤去了計算平方根這步,使得算法的性能大大變差了,達到耗時的目的。為了拖慢時間,我們找一個巨大的質數1073676287,這樣,整個for循環要全部運作一次才會結束,而不會提早break。

為了異步調用,要先聲明一個和方法簽名相同的委托才行:

public delegate void ClongBigFileDelegate(string path);      

然後,我們就在主程式中簡單的異步調用。我們發現BeginInvoke的參數數目比Invoke多了兩個,不過現在我們先不管它,将它們都設定為null:

  IsPrimeNumberDelegate d = new IsPrimeNumberDelegate(IsPrimeNumber);
  d.BeginInvoke(1073676287, null, null);
    Console.WriteLine("I am doing something else.");
    Console.ReadKey();      

這樣雖然實作了異步調用(主程式會馬上離開BeginInvoke列印下面的話),但也有很多問題:

  • 如果不加上Console.ReadKey,主程式會直接關閉,因為唯一的前台線程結束運作了(winform則不存在這個問題,除非你終止程式,前台線程永遠不會結束運作)
  • 異步調用具體什麼時候結束工作不知道。可能很快就結束了,可能剛進行了5%,總之就是看不出來(但如果你手賤敲了任意一個鍵,程式立馬結束),也不能實作“當異步調用結束之後,主程式繼續運作某些代碼”
  • 算了半天,不知道結果...

你可能也想到了,BeginInvoke後兩個神秘的輸入參數可能能幫你解決上面的問題。

通過EndInvoke獲得異步委托的執行結果

我們可以通過EndInvoke獲得委托标的函數的傳回值:

委托和事件
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();      
委托和事件

這解決了第一個問題和第三個問題。現在你再運作程式,程式會阻塞在EndInvoke,你手賤敲了任意一個鍵,程式也不會結束。另外,我們還獲得了異步委托的結果,即該大數是質數。

但這個解決方法又衍生出了一個新的問題:即程式會阻塞在EndInvoke,如果這是一個GUI程式,主線程将會卡死,給使用者帶來不好的體驗。如何解決這個問題?

通過回調函數獲得異步委托的執行結果

回調函數的用處是當委托完成時,可以主動通知主線程自己已經完成。我們可以在BeginInvoke中定義回調函數,這将會在委托完成時自動執行。

回調函數的類型是AsyncCallback,其也是一個委托,它的簽名:傳入參數必須是IAsyncResult,而且沒有傳回值。是以我們的回調函數必須長成這樣子:

public static void IsPrimeNumberCallback(IAsyncResult iar)
{
}      

在主函數中加入回調函數:

AsyncCallback acb = new AsyncCallback(IsPrimeNumberCallback);
d.BeginInvoke(1073676287, acb, null);       

IAsyncResult中并不包括委托的傳回值。利用AsyncCallback可以被轉換成AsyncResult類型的特點,我們可以利用AsyncResult中的AsyncDelegate“克隆”一個目前正在運作的委托,然後調用克隆委托的EndInvoke。因為這時委托已經執行完了是以EndInvoke不會阻塞:

委托和事件
public static void IsPrimeNumberCallback(IAsyncResult iar)
        {
            AsyncResult ar = (AsyncResult) iar;
            var anotherDelegate = (IsPrimeNumberDelegate) ar.AsyncDelegate;
            var ret = anotherDelegate.EndInvoke(iar);
            Console.WriteLine("Calculation finished, Number is prime number : {0}", ret == true ? "Yes" : "No");
        }      
委托和事件

看到這裡讀者大概要感慨了,使用委托異步調用獲得結果怎麼這麼複雜。确實是比較複雜,是以之後微軟就在後續版本的C#中加入了任務這個工具,它大大簡化了異步調用的編寫方式。

總結

使用委托的異步程式設計模型(APM):

  1. 通過建立一個委托和使用BeginInvoke調用委托來實作異步,通過EndInvoke來獲得結果,但要注意的是,EndInvoke會令主線程進入阻塞狀态,卡死主線程,是以我們通常使用回調函數
  2. BeginInvoke方法擁有委托全部的輸入,以及額外的兩個輸入
  3. 第一個輸入為委托的回調函數,它是AsyncCallback類型,這個類型是一個委托,其輸入必須是IAsyncResult類型,且沒有傳回值,如果需要獲得傳回值,需要在回調函數中,再次呼叫EndInvoke,并傳入IAsyncResult
  4. 委托的回調函數在次線程任務結束時自動執行,并替代EndInvoke
  5. 第二個輸入為object類型,允許你為異步線程傳入自定義資料
  6. 因為使用委托的異步調用本質上也是通過線程來實作異步程式設計的,是以也可以使用同Threading相同的取消方法,但這實在是太過麻煩(你需要手寫一個CancellationToken,這部分到說到線程的時候再說)
  7. 關于進度條的問題,要等到更進階的BackgroundWorker來解決
  8. 我們看到擷取異步結果這一步還是比較麻煩,是以在任務和BackgroundWorker等大殺器出現之後,這個模型就基本不會使用了

多路廣播

委托的本質是一個密封類。這個類繼承自System.MultiDelegate,其再繼承自System.Delegate。System.MulticastDelegate類中有一個重要字段_invocationList,它令委托可以挂接多于一個函數(即一個函數List)。它維護一個Invocation List(委托鍊)。你可以為這個鍊自由的添加或删除Handler函數。一個委托鍊可以沒有函數。

由于委托可以代表一類函數,你可以随心所欲的為委托鍊綁定合法的函數。此時如果執行委托,将會順序的執行委托鍊上所有的函數。如果某個函數出現了異常,則其後所有的函數都不會執行。

如果你的委托的委托鍊含有很多委托的話,你隻會收到最後一個含有傳回值的委托的傳回值。假如你的委托是有輸出值的,而且你想得到委托鍊上所有方法的輸出值,你隻能通過GetInvocationList方法得到委托鍊上的所有方法,然後一一執行。

委托的本質

本節大部分都是概念,如果你正在準備面試,而且已經沒有多少時間了,可以考慮将它們背下來。

  • 委托的本質是一個密封類。這個類繼承自System.MultiDelegate,其再繼承自System.Delegate。這個密封類包括三個核心函數,Invoke方法賦予其同步通路的能力,BeginInvoke,EndInvoke賦予其異步通路的能力。例如public delegate int ADelegate(out z,int x,int y)的三個核心函數:
    • int Invoke (out z,int x,int y)
    • IAsyncResult BeginInvoke (out z,int x,int y,AsyncCallback cb,object ob)
    • int EndInvoke (out z,IAsyncResult result)
    • Invoke方法的參數和傳回值同委托本身相同,BeginInvoke的傳回值總是IAsyncResult,輸入則除了委托本身的輸入之外還包括了AsyncCallback(回調函數)和一個object。EndInvoke的輸入總是IAsyncResult,加上委托中的out和ref(如果有的話)類型的輸入,輸出類型則是委托的輸出類型。
  • 在事件中,委托是事件的發起者sender将EventArgs傳遞給處理者的管道。是以委托是一個密封類,沒有繼承的意義。
  • 委托可以看成是函數指針,它接受與其簽名相同的任何函數。委托允許你把方法作為參數。
  • 相比C的函數指針,C#的委托是類型安全的,可以友善的獲得回調函數的傳回值,并且可以通過委托鍊支援多路廣播。
  • EventHandler委托類型是.NET自帶的一個委托。其不傳回任何值,輸入為object類型的sender和EventArgs類型的e。如果你想傳回自定義的資料,你必須繼承EventArgs類型。這個委托十分适合處理不需要傳回值的事件,例如點選按鈕事件。
  • System.MulticastDelegate類中有一個重要字段_invocationList,它令委托可以挂接多于一個函數(即一個函數List)。它維護一個Invocation List(委托鍊)。你可以為這個鍊自由的添加或删除Handler函數。一個委托鍊可以沒有函數。添加或删除實質上是調用了Delegate.Combine / Delegate.Remove。
  • 當你為一個沒有任何函數的委托鍊删除方法時,不會發生異常,僅僅是沒有産生任何效果。
  • 假設委托可以傳回值,那麼如果你的委托的委托鍊含有很多委托的話,你隻會收到最後一個委托的傳回值。
  • 如果在委托鍊中的某個操作出現了異常,則其後任何的操作都不會執行。如果你想要讓所有委托挂接的函數至少執行一次,你需要使用GetInvocationList方法,從委托鍊中獲得方法,然後手動執行他們。

泛型委托

泛型委托Action和Func是兩個委托,Action<T>接受一個T類型的輸入,沒有輸出。Func則有一個輸出,16個重載分别對應1-16個T類型的輸入(這使得它更像數學中函數的概念,故名Func)。Func委托的最後一個參數是傳回值的類型,前面的參數都是輸入值的類型。

在它們出現之後,你就不需要使用delegate關鍵字聲明委托了(即你可以忘記它了),你可以使用泛型委托代替之。

委托和事件
    static void Main(string[] args)
       {
            Action<int, int> a = new Action<int, int>(add);
            a(1, 2);
            //Func委托的最後一個參數是傳回值的類型
            Func<int, int, int> b = new Func<int, int, int>(add2);
            Console.WriteLine(b(1, 2));
            Console.ReadLine();
        }
        //這個EventHandler不傳回值
        public static void add(int a, int b)
        {
            Console.WriteLine(a + b);
        }
        //這個EventHandler傳回一個整數
        public static int add2(int a, int b)
        {
            return a+b;
        }      
委托和事件

我們可以看到使用Action對代碼的簡化。我們不用再自定義一個委托,并為其取名了。這兩個泛型委托構成了LINQ的基石之一。

委托和事件

我們看一個LINQ的例子:Where方法。

委托和事件

通過閱讀VS的解釋,我們可以獲得以下資訊:

  1. Where是IEnumerable<T>的一個擴充方法
  2. 這個方法的輸入是一個Func<T,bool>,形如Func<T,bool>的泛型委托又有别名Predicate,因其是傳回一個布爾型的輸出,故有判斷之意。

泛型委托使用一例

下面這個問題是某著名公司的一個面試題目。其主要的問題就是,如何對兩個對象比較大小,這裡面的對象可以是任意的東西。這個題目主要考察的是如何使用泛型和委托結合,實作代碼複用的目的。

假設我們有若幹個表示形狀的結構體,我們要比較它們的大小。

委托和事件
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;
        }
    }      
委托和事件

我們規定誰面積大就算誰大,此時,因為結構體不能比較大小,隻能比較是否相等,我們就需要自己制定一個規則。對不同的形狀,求面積的公式也不一樣:

委托和事件
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;
        }      
委托和事件

當然,在比較大小的時候,可以直接調用這些函數。但如果這麼做,你将再次陷入“委托的作用-将方法作為方法的參數”一節中的switch泥潭。注意到這些函數的簽名都相同,我們現在已經熟悉委托了,當然就可以用委托來簡化代碼。 

我們可以把規則看作一個函數,其輸入為兩個同類型的對象,輸出一個整數,當地一個對象較大時輸出1,相等輸出0,第二個對象較大輸出-1。那麼,這個規則函數的簽名應當為:

Func<T, T, int>      

它可以變身為任意類型的比較函數。我們在外部再包裝一下,将這個規則傳入進去。那麼這個外部包裝函數的簽名應當為:

public static void Compare<T>(T o1, T o2, Func<T, T, int> rule)
{
}      

當然這裡的傳回值也可以是int。由于是示範的緣故,我就簡單的列印一些資訊:

委托和事件
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.");
        }      
委托和事件

主程式調用:

委托和事件
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();
        }      
委托和事件

我們可以看到,對不同類型都有着統一的比較大小的方式。可以參考:http://www.cnblogs.com/onepiece_wang/archive/2012/11/28/2793530.html 

什麼是事件?

簡單的看,事件的定義就是通知(給訂閱者)。事件由三部分組成:事件的觸發者(sender),事件的處理者(Event Handler,一個和委托類型相同的函數)和事件的資料傳送通道delegate。delegate負責傳輸事件的觸發者對象sender和自定義的資料EventArgs。要實作事件,必須實作中間的委托(的标的函數),并為事件提供一個處理者。處理者函數的簽名和委托必須相同。

是以,事件必須基于一個委托。

使用事件的步驟:

  1. 聲明委托(指出當事件發生時要執行的方法的方法類型)。委托要傳遞的資料可能是自定義類型的
  2. 聲明一個事件處理者(一個方法),其簽名和委托簽名相同
  3. 聲明一個事件(這需要第一步的委托)
  4. 為事件+=事件處理者(委托對象即是訂閱者/消費者)
  5. 在事件符合條件之後,調用事件
委托和事件

委托和事件有何關系?

委托是事件傳輸消息的管道。事件必須基于一個委托。下圖中小女孩是事件的發起者(擁有者),她通過委托(即圖上的“電話線”)傳遞若幹消息給她的爸爸(事件的處理者/訂閱者)。和委托一樣,事件可以有多個訂閱者,這也是多路廣播的一個展現。

可以借助事件實作觀察者模式。觀察者模式刻畫了一個一對多的依賴關系,其中,當一對多中的“一”發生變化時,“多”的那頭會收到資訊。

委托和事件

經典例子:this.button1.Click += new System.EventHandler(this.StartButton_Click);

  • Click是一個事件,它的定義為public event EventHandler Click,它基于的委托類型是EventHandler類型。
  • Click事件挂接了一個新的委托,委托傳遞object類型的sender和EventArgs類型的e給事件的處理者StartButton_Click。StartButton_Click是一個和EventHandler委托類型簽名相同的函數。
  • EventHandler是.NET自帶的一個委托。其不傳回任何值,輸入為object類型的sender和EventArgs類型的e。EventArgs類型本身沒有任何成員,如果你想傳遞自定義的資料,你必須繼承EventArgs類型。

使用事件

使用事件需要至少一個訂閱者。訂閱者需要一個事件處理函數,該處理函數通常要具備兩個參數:輸入為object類型的sender和一個繼承了EventArgs類型的e(有時候第一個參數是不必要的)。你需要繼承EventArgs類型來傳遞自定義資料。

委托和事件
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; }
    }      

當有訂閱者訂閱事件之後,Invoke事件會順序激發所有訂閱者的事件處理函數。其激發順序視訂閱順序而定。

首先要定義委托和事件。委托的命名慣例是以Handler結尾:

//1. Base delegate
        public delegate void SendMessageHandler(object sender, MessageArgs e);

        //2. Event based on the delegate
        public static event SendMessageHandler SendMessage;      

事件的執行示範:

委托和事件
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();
        }      
委托和事件

事件的本質

  • 如果你檢視事件屬性的對應IL,你會發現它實質上是一個私有的字段,包含兩個方法add_[事件名]和remove_[事件名]。
  • 事件是私有的,它和委托的關系類似屬性和字段的關系。它封裝了委托,使用者隻能通過add_[事件名]和remove_[事件名](也就是+=和-=)進行通路。
  • 如果訂閱事件的多個訂閱者在事件觸發時,有一個訂閱者的事件處理函數引發了異常,則它将會影響後面的訂閱者,後面的訂閱者的事件處理函數不會運作。
  • 如果你希望事件隻能被一個客戶訂閱,則你可以将事件本身私有,然後暴露一個注冊的方法。在注冊時,直接使用等号而不是+=就可以了,後來的客戶會将前面的客戶覆寫掉。

委托的協變和逆變

協變和逆變實際上是屬于泛型的文法特性,由于有泛型委托的存在,故委托也具備這個特性。我将在讨論泛型的時候再深入讨論這個特性。

經典文章,參考資料

有關委托和事件的文章多如牛毛。熟悉了委托和事件,将會對你了解linq有很大的幫助。

1. 張子陽的經典例子:  http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html

    可以自行編寫一個熱水器的例子,測試自己是否掌握了基本的事件用法。

    http://www.cnblogs.com/JimmyZhang/archive/2008/08/22/1274342.html 這是續篇。

上一篇: 泛型
下一篇: 2022年第一天