《C#高級編程》【第八章】委託、lambda表達式和事件 -- 學習筆記

       之前由於考試的關係,耽誤了不少時間。然而考試也考的不怎麼樣,說多了都是淚哭。下面我們直接進入今天的正題 --- 委託。

       委託是一個神奇的東西。委託的出現,使得方法可以作爲參數進行傳遞。其中我們接觸最多的應該就是通用庫類。通用庫類,正是由於這種機制才實現了其的通用性。

一、普通委託

        委託類由關鍵字delegate來聲明。我們先看看,定義一個委託類的語法:

[訪問限制符] delegate [返回值類型] [委託類的名稱]( [參數列表] );
       實際上這裏隱藏了一個派生關係----委託類派生自基類System.MulticastDelegate。然而我們只需知道有這麼回事就好了,其餘的編譯器會幫我們完成。

1、單路委託

       我們已經知道了委託類的定義,那麼我們來具體看看委託使用的一個實例吧:

public delegate int Calc(int a, int b);	//定義了一個委託類Calc

       假設我們存在方法int add(int x, int y)和方法int Mul(int a, int b)。假定這兩個方法分別是返回兩數之和與兩數之積。

Calc TestDele = new Calc(add);	//委託對象的初始化和賦值和普通類一致

       現在我們創建了一個委託對象TestDele併爲將方法add作爲初值賦給TestDele。從這裏我們又可以說明一個委託的性質-----作爲值賦給委託的方法必須與委託類的返回值類型,參數類型一致。只有滿足這兩個條件可以將值賦給委託類的對象,否則編譯器將會報錯。

TestDele(1, 2);		//現在這個語句就等價於 add(1, 2)

       我們除了上面這種方式調用方法,我們還可以使用Invoke()方法來實現上述的功能。換而言之,TestDele.Invoke()和TestDele()是完全等價的。(其實接觸過C\C++的同學會發現,委託的本質上其實就是C\C++中的函數指針,唯一不同點就是委託比函數指針安全)

注意:我們通常將委託類和委託類的對象都成爲委託,但是兩者是有區別的。一旦定義了委託類,基本上就可以實例化它的實例,在這一點上和普通類似一致的。即我們也可以有委託數組。

2、多播委託

        然而委託除了可以與方法建立一對一的關係,它還可以和方法有一對多的關係。我們稱這種委託爲多播委託。我們使用+=和-=來是實現增加方法和刪除方法的功能。我們繼續以上面的例子爲例,在上面的基礎上我們給委託對象加入第二種方法:

TestDele += Mul;	//新加入的方法也必須滿足返回值類型和參數列表與委託一致
TestDele(1, 2);

       現在我們調用方法的話,那麼編譯器就會分別調用add和Mul方法,但是我們要注意兩點(1、我們無法保證調用的順序  2、如果是有返回值的多播委託,那麼委託的返回值將是最後一個加入的方法)。
       我們在使用多播委託的過程中可能會出現委託鏈中的某個方法拋出異常。那麼這時委託的迭代就會停止。我們爲了避免這個問題,Delegate類就定義GetInvocationList()方法,它返回一個Delegate對象數組。我們還是用上述的Calc委託類作爲實例:

Delegate[] delegates = TestDele.GetInvocationList();	
//將TestDele的方法列表傳給Delegate數組delegates
foreach(Calc d in delegates){
	try{
		d(1 , 3);
	}
	catch(Excetion){
	//異常處理代碼
	}
}

        這樣的話,程序運行過程中在捕獲異常之後還會繼續迭代下一個方法。

3、Action<T>和Func<T>委託

       我們之前都是根據返回值類型和參數列表來定義委託類,然後在根據委託類來生成委託的實例。現在我們還可以使用泛型委託類Action<T>和Func<T>。
       泛型Action<T>委託類表示引用一個void返回類型的方法,可以傳遞至多16個不同的參數類型。Action<in T1,in T2, …,in Tn> (n最大爲16,例如Action<in T1,in T2>就表示調用2個參數的方法)。Func<T>委託的使用方式和Action<T>委託類似.Func<T>允許調用帶有返回值的方法。Func<in T1, in T2, ...,in Tn, out TResult> (n的最大值還是16,Func<in T1, in T2, out TResult>表示調用兩個參數的方法且返回值類型爲TResult)。

我們用Func<T>委託來實現上述Calc委託:

Func<int, int, int> TestDele = add;

      這一條語句就等價於委託類的聲明和委託對象的創建。同理,Action<T>也是一樣的用法。而且功能上沒有任何的不同。唯一不足之處就是參數的個數是有限制的,不過大多數的情況下16個的參數已經足夠使用了。

二、匿名委託和lambda表達式

1、匿名委託

       在這之前我們使用委託那麼都必須先有一個方法。那麼現在我們可以通過另一種方式使委託工作:匿名方法。用匿名方法來實現委託和之前的定義並沒有太大的區別,唯一不同之處就在於實例化。我們就以之前Calc委託類爲例:

Calc TestDele = delegate(int a,int b)
{
	//代碼塊(因爲Calc委託類是有返回值的,所以函數體內必須有return語句)
};	//這裏有一個分號,千萬不能漏

      通過使用匿名方法,由於不必創建單獨的方法,因此減少了實例化委託所需的編碼系統開銷。而且使用匿名方法可以有效減少要編寫的代碼,有助於降低代碼的複雜度。
      然而我們在使用匿名委託的時候我們要遵守兩個原則:1、匿名方法中不能有跳轉語句(break, goto或continue)跳轉到匿名方法的外部,反之,外部代碼也不能跳轉到該匿名方法內部。2、在匿名方法中不能訪問不安全代碼。
注意:不能訪問在匿名方法外部使用的ref和out參數。

2、Lambda表達式

      由於Lambda表達式的出現使得我們的代碼可以變得更加的簡潔明瞭。我們現在來看看Lambda表達式的使用語法:

[委託類] [委託對象名] = ( [參數列表] ) => { /*代碼塊*/ };	   //結尾還是有一個分號

       我們值得注意的是lambda表達式的參數列表,我們只需給出變量名即可,其餘的編譯器會自動和委託類進行匹配。如果委託類使用返回值的,那麼代碼塊就需要return一個返回值。我們用一個例子來說明上述問題:

Func<int, int> TestLam = (x) => { return x*x; };

       在這裏我們是使用一個參數的爲例,上面的寫法是Lambda表達式的正常寫法,但是當參數只有一個時,x兩邊的括號就可以去除,那麼現在代碼就變成這樣了:

Func<int, int> TestLam = x => { return x*x; };

        當Lambda表達式代碼塊中只有一條語句,那麼我們就可以把花括號丟了。如果這一條語句還是包含return的語句,那麼我們在去除花括號的同時,必須將return同時刪去。現在上述代碼就變成了這樣:

Func<int, int> TestLam = x => x*x;

注意:Lambda表達式可以用於類型爲委託的任意地方。

3、閉包

       通過lambda表達式可以訪問lambda表達式外部的變量,於是我們就引出了一個新的概念-----閉包。我們來看一個例子:

int someVal = 5;
Func<int, int> f = x => x+someVal;

      現在我們很容易知道f(3)的返回值是8,我們繼續:

someVal = 7;

       我們現在將someVal的值改爲7,那麼這時我們在調用f(3),現在就會很神奇的發現f(3)的返回值變成了10。這就是閉包的特點,這個特點在編程上很大程度上能給我們帶來一定的好處。但是有利終有弊,如果我們使用不當,那麼這就變成了一個非常危險的功能。
       我現在再來看看在foreach語句中的閉包,我們現在看看下面這段代碼:

List<int> values = new List<int>() { 10, 20, 30 };
var funcs = new List<Func<int>>();
foreach (var val in values)
{
   funcs.Add(() => val);
}
foreach (var f in funcs)
{
   Console.WriteLine(f());
}

        用我們剛纔的知識來判斷的話,輸出結果應該是3個30。然而在C#4.0確實是這樣,然而C#5.0會在foreach創建的while循環的代碼塊中創建一個不同的局部循環變量,所以這時在C#5.0中我們輸出的結果應該是分別輸出10,20和30。

三、事件

        事件是一種特殊多播委託,換句話來說,事件是經過深度封裝的委託。一個事件簡單的可以看作一個多播委託加上兩個方法(+=訂閱消息和-=取消訂閱)。

1、普通事件

        我們使用event關鍵字來聲明事件,語法如下:

[訪問權限修飾符] event [委託類類名] [名稱];

       事件一般是用於通知代碼發生了什麼。由此我們又可以引出兩個概念:1、事件發佈方2、事件偵聽方。我們現在用一個簡單的例子來說明這兩個概念,我們以燒開水爲例,當水溫爲95至100度時發出警報。我們先來定義在事件發生時,需要傳輸的數據成員:

public class Water	//事件發佈程序中的基本數據成員類
{
    public int Temperature { get; private set; }
    public Water(int t)
    {
        this.Temperature = t;
    }
}

有了傳輸的數據,那麼我們現在就可以定義事件觸發類::

public delegate void WaterHandler(object sender, Water w);     //sender爲事件發送者,w爲發送的數據
public class Heater	//事件發佈程序中的,事件觸發類
{
    public event WaterHandler WaterEvent;    //深度的封裝委託
    public void HeatWater()	//該方法用於觸發事件
    {
        for (int i = 0; i < 101; i++)
        {
            if (i > 95 && i < 101)
            {
                RegWaterEvent(i);		//觸發事件
            }
        }
    }
    protected virtual void RegWaterEvent(int t)
    {
        WaterHandler temp = WaterEvent;
        if (temp != null)	
            temp(this, new Water(t));	//如果委託不爲空,我們就執行委託,我們無需知道具體執行了哪些方法
    }
}

       現在我們已經完整了定義好了事件發佈方了,通過這個例子我們也知道了事件發佈方由兩部分組成:1、基本數據類   2、事件觸發類。接下來我們繼續看看事件偵聽方又是怎麼樣的:

public class Alarm	//事件偵聽類
{
    public void Waring(object sender, Water w)		//偵聽接口,由於偵聽事件的發佈
    {
        Console.WriteLine("當前水溫已經到達 {0} ℃!", w.Temperature);
    }
}

      通過這個例子我們可以發現,事件偵聽類,只需有一個和被監聽事件一致的方法即可。

Heater heater = new Heater();		//生成事件發佈實例
Alarm alarm = new Alarm();
heater.WaterEvent += alarm.Waring;	//對事件發佈方進行訂閱(偵聽),反之我們使用-=取消訂閱
heater.HeatWater();			//觸發事件,那麼現在Alarm類對象alarm將會偵聽到這次事件

        通過上述例子我們就大致的瞭解了事件的工作情況,以及事件發佈方和事件偵聽方的概念。

       .Net平臺爲我們提供了泛型委託EventHandler<T>,有了這個泛型委託之後我們就不在需要定義委託類了。我們來看看泛型委託EventHandler<T>的原型:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs: EventArgs;
       參數列表中第一個參數是對象,包含事件的發送者,第二個參數提供了事件的相關信息。現在我們定義事件時,只需讓基本數據類繼承EventArgs,然後我們就能泛型委託來定義事件了。
注意:事件只能在本類型內部“觸發”,委託不管在本類型內部還是外部都可以“調用”。事件在類的外部只能使用+=或-=來增加/取消訂閱。

2、弱事件

        我們通過事件,將事件發佈方(source)與事件偵聽方(listener)連接在一起。但是現在問題來了。當事件發佈方(source)比事件偵聽方(listener)具有更長的生命期,且事件偵聽方沒有被其他對象引用也不需要改事件。由於事件發佈方還有保存着偵聽方的一個引用,這時就會導致垃圾回收器不能清空事件偵聽器所佔用的內存。於是,就發生內存泄露現象。

<1>弱事件管理器

        每當偵聽器需要註冊事件,而該偵聽器並不明確瞭解什麼時候註銷時,就可以使用弱事件模式。我們這時只要讓事件發佈方的變爲弱引用,那麼在我們不使用偵聽器的時候,垃圾回收機制就可以發揮它的作用了。.Net平臺爲我們聽過了WeakEventManager類作爲發佈程序與偵聽器之間的中介,也就是弱事件管理器。現在我們增加/取消訂閱通過WeakEventManager的方法AddListener和RemoveListener來實現,這樣發佈程序的引用就變爲了弱引用。要使用弱事件那麼就需要一個派生自WeakEventManager類(System.Windows名稱空間中)的類,不僅如此還需要讓偵聽器實現接口IWeakEventsListener。

        我們就以上述燒開水的爲例,在定義一個弱事件管理器類WeakBoilWaterEventManager之前,我們得先把上述例子的事件用泛型委託EventHandler<T>重新定義(在此就不寫代碼了),然後在定義WeakBoilWaterEventManager:

class WeakBoilWaterEventManager : WeakEventManager	//繼承自WeakEventManager的弱事件管理類
{
    public static void AddListener(object source, IWeakEventListener listener)	//增加訂閱
    {
        CurrentManager.ProtectedAddListener(source, listener);	//將提供的偵聽器(listener)添加到爲託管事件所提供的事件發佈方(source)中。
    }
    public static void RemoveListener(object source, IWeakEventListener listener) //取消訂閱
    {
        CurrentManager.ProtectedRemoveListener(source, listener);  //從提供事件發佈方的中移除以前添加的偵聽器。
    }
    public static WeakBoilWaterEventManager CurrentManager     //WeakBoilWaterEventManager的實例
    {
        get
        {
            var manager = GetCurrentManager(typeof(WeakBoilWaterEventManager)) as WeakBoilWaterEventManager;
            if (manager == null)
            {
                manager = new WeakBoilWaterEventManager();
                SetCurrentManager(typeof(WeakBoilWaterEventManager), manager);
            }
            return manager;
        }
    }
    void Heater_WaterEvent(object sender, Water w)
    {
        DeliverEvent(sender, w);	//將正在託管的事件傳送到每個偵聽器。
    }
    protected override void StartListening(object source)    //開始偵聽被託管的事件
    {
        (source as Heater).WaterEvent += Heater_WaterEvent;
    }
    protected override void StopListening(object source)    //停止偵聽被託管的事件
    {
        (source as Heater).WaterEvent -= Heater_WaterEvent;
    }
}

        現在我們就創建好了一個弱事件管理類,因爲它是管理事件WaterEvent和偵聽器之間的連接,所以我們把這個類實現爲單態模式,即我們繼續創建一個實例---靜態屬性CurrentManager。它用於訪問弱事件管理器類中的單態對象。

        現在我們光有弱事件管理類還不夠,我們還需要讓偵聽器實現IWeakEventListener的接口。該接口只有一個ReceiveWeakEvent方法。觸發事件時從弱事件管理器中調用這個方法。

public class Alarm : IWeakEventListener
{
    public void Waring(object sender, Water w)
    {
        Console.WriteLine("當前水溫已經到達 {0} ℃!", w.Temperature);
    }

    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        Waring(sender, e as Water);
        return true;
    }
}

       現在萬事俱備了,接下我們只需要使用AddListener和RemoveListener方法來進行增加/取消訂閱即可:

Heater heater = new Heater();
Alarm alarm = new Alarm();
WeakBoilWaterEventManager.AddListener(heater, alarm);
heater.HeatWater();

       現在事件發佈方和事件偵聽方之間不再是強連接,當不再引用偵聽器時,他就會被垃圾回收。
<2>泛型弱事件管理器
       通過剛纔的例子我們可以發現,像這樣處理弱事件十分的麻煩。於是於是.Net平臺提供了泛型版本的弱事件管理器。泛型類WeakEventManager<TEventSource, TEventArgs>派生自WeakEventManager,它簡化我們弱事件的處理。使用這個類時不需要在爲每個事件定義弱事件管理器,也不需要讓偵聽器實現接口IWeakEventsListener。我們只需使用AddHandler和RemoveHandler來實現增加/取消訂閱。
     我們還是使用燒開水的那個例子來說明(事件WaterEvent還是要用泛型委託EventHandler<T>來實現):
Heater heater = new Heater();
Alarm alarm = new Alarm();
WeakEventManager<Heater, Water>.AddHandler(heater, "WaterEvent", alarm.Waring);
heater.HeatWater();
      看現在我們只需要一條語句就可以了,然而程序的工作方式又和之前一樣,代碼卻少了一大堆。
(如有錯誤,歡迎指正,轉載請註明出處)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章