1. 程式人生 > >《C#高階程式設計》【第八章】委託、lambda表示式和事件 -- 學習筆記

《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();
      看現在我們只需要一條語句就可以了,然而程式的工作方式又和之前一樣,程式碼卻少了一大堆。 (如有錯誤,歡迎指正,轉載請註明出處)