1. 程式人生 > >委託與事件(1)

委託與事件(1)

原創連結



網上講C#委託和事件的博文已經非常多了,其中也不乏一些深入淺出、條理清晰的文章。我之所以還是繼續寫,主要是藉機整理學習筆記、歸納總結從而理解更透徹,當然能夠以自己的理解和思路給其他人講明白更好。
另外,太長的文章會讓很多讀者失去興趣,所以我決定把這篇分成四個部分來介紹。分別是委託的基礎、委託的進階、事件的基礎和事件的進階。對使用委託與事件要求不高的同學可以跳過進階部分。

首先,本小節我們來介紹一下委託最最基礎的部分,在列舉這些基礎知識之前,我們先從例項出發看看為什麼要使用委託,以及什麼情況下需要使用委託。

1. 為什麼要使用委託?

假設我們有一個這樣的需求,寫一個MakeGreeting函式,這個函式在被呼叫的時候需要告訴它兩點:跟誰greet怎麼greet
我們的第一反應可能是,很簡單呀,給這個函式傳兩個引數,就傳跟誰greet怎麼greet。如果怎麼greet只是一個string,當然可以這樣做,可萬一它們沒那麼簡單呢?
繼續假設,假設怎麼greet只有兩種情況HelloGoodbye,分別是下面程式碼中的兩個函式。(雖然函式裡只寫了一句輸出,但是我們假設它們還要做一些其他事情,只是沒有寫出來而已。要不然可能有人會疑問為什麼要搞這麼複雜啦)

根據上面的需求描述,完成第一版程式:

namespace TestDelegate
{
    public
enum Greeting { Hello, Goodbye } class Program { public static void Hello(string s) { Console.WriteLine(" Hello, {0}!", s); // do something (hug or shake hand...) } public static void Goodbye(string s) { Console.WriteLine(" Goodbye, {0}!"
, s); // do something (hug or wave hand...) } static void MakeGreeting(string name, Greeting greeting) { switch (greeting) { case Greeting.Hello: Hello(name); break; case Greeting.Goodbye: Goodbye(name); break; } } static void Main(string[] args) { MakeGreeting("May", Greeting.Hello); MakeGreeting("April", Greeting.Goodbye); } } }

輸出內容:

 Hello, May!
 Goodbye, April! ```

這樣寫當然是可以的,只是擴充套件性並不好,如果需要再加更多的`Greeting`就需要改三個地方:(1) 新增`Greeting`相關的方法、(2) `Greeting`列舉裡新增值、(3) 在`MakeGreeting`函式的`switch`語句裡新增對新增`Greeting`的處理。
也就是說每增加一個`Greeting`方法時,還需要增加列舉並在`MakeGreeting`裡面把新增的方法與列舉值關聯起來。

那麼問題來了,我們可不可以直接把`Greeting`方法(如`Hello`, `Goodbye`)傳進`MakeGreeting`函式裡呢?像`C++`裡的函式指標那樣。這樣就不需要`Greeting`列舉,也不需要在`MakeGreeting`函式裡面進行`switch`選擇了。
答案當然是可以的,委託就可以是一系列類似方法(這裡類似是指引數值列表和返回值可以用一個模板表示出來)的類,它的物件就是不同的方法,所以可以用委託把這一系列`Greeting`方法(物件)的共性(類)定義出來,然後給`MakeGreeting`函式傳遞一個該委託(共性類)的物件(就是一個`Greeting`方法)。

於是,就有了下面利用委託來完成上述需求的第二版程式:

namespace TestDelegate
{
delegate void GreetingDelegate(string s); //宣告委託,定義Greeting方法的類
class Program
{
public static void Hello(string s)
{
Console.WriteLine(” Hello, {0}!”, s);
// do something (hug or shake hand…)
}
public static void Goodbye(string s)
{
Console.WriteLine(” Goodbye, {0}!”, s);
// do something (hug or wave hand…)
}
static void MakeGreeting(string name, GreetingDelegate d)
{
d(name);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定義委託的一個物件(將方法繫結到委託)
GreetingDelegate d2 = Goodbye; //定義委託的另一個物件
MakeGreeting(“May”, d1);
MakeGreeting(“April”, d2);
}
}
}“`
輸出內容:

 Hello, May! 
Goodbye, April! `</span><span class="hljs-string">

小結:如何實現一個委託
(1) 宣告一個</span>delegate<span class="hljs-string">物件,它與我們想要定義的一系列方法具有相同的引數和返回值型別。如:
</span>public delegate void GreetingEventHandler(string name)<span class="hljs-string">
(2) 委託的例項化。建立</span>delegate<span class="hljs-string">物件,並將我們想要使用的方法繫結到委託。(下面會在基礎知識裡面細講委託例項化與類例項化的區別,以及講方法繫結到委託的不同方法)
(3) 使用委託。使用委託中繫結的方法時,直接傳遞繫結有該方法的委託物件,或者直接通過委託物件呼叫繫結的方法。
例如:上例中我們可以把繫結有</span>Hello<span class="hljs-string">方法的委託物件</span>d1<span class="hljs-string">傳遞給</span>MakeGreeting<span class="hljs-string">函式,在函式內實現方法的呼叫。還可以直接通過</span>d1<span class="hljs-string">來呼叫方法,即</span>d1(<span class="hljs-string">"May"</span>);<span class="hljs-string">會直接輸出</span>Hello, May!<span class="hljs-string">

2. 什麼情況下使用委託?

上面的例子已經給出了一種情況,就是我們需要在執行時動態地確定具體的呼叫方法。其實這種簡單的情況用介面也可以實現,因為介面也是一系列相似方法的抽象,類的繼承與多型也可以實現執行時呼叫不同的方法。所以像上例中的情況下,何時該用委託呢?

a) 當使用事件設計模式時。

就是</span>Observer<span class="hljs-string">設計模式,它定義了物件之間一對多的關係,並且通過事件觸發機制關聯它們。當一個物件中發生了某事件後,依賴它的其他物件會被自動觸發並更新。在事件部分我們會更細緻地介紹。

b) 當需要封裝靜態方法時。

委託繫結的方法可以是靜態方法、非靜態方法和匿名方法,而C#中介面不能是靜態的。

c) 當呼叫方不需要訪問實現該方法的物件中的其他屬性、方法或介面時。

我們可以把一個類中的某個成員函式繫結到委託,呼叫該方法時只與這個成員函式有關,與該類裡的其他屬性無關。

d) 當需要方便的組合時。

一個委託繫結的多個方法可以自由組合。一個委託的物件可以繫結多個方法,而且這多個方法是沒有限制可以任意組合的,委託靈活的繫結和解繫結策略使得使用非常方便。

e) 當類可能需要該方法的多個實現時。

一個委託的物件可以繫結多個方法,當我們執行時需要的不是單一的方法時,介面很難實現。

舉例說明使用委託傳送物件狀態通知。我直接以</span>《精通C<span class="hljs-comment">#》中的一個例子來說明這種用法。
我們有一個Car型別,在Car中定義一個委託並封裝一個委託的物件(這可以用事件實現,目前先這樣寫,實際上這樣做是不對的)。然後我們通過委託來向外界傳送物件狀態的通知。

namespace TestDelegate2
{
public class Car
{
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; }
public string PetName { get; set; }
public Car() { MaxSpeed = 100; }
public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;
PetName = name;
}
// declare a delegate type
public delegate void CarEngineHandler(string message);
// Create a new delegate object*
private CarEngineHandler listOfHandlers;
// associate with method*
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers += methodToCall;
}
public void Accelerate(int delta)
{
if(CurrentSpeed >= MaxSpeed)
{
if(listOfHandlers != null)
{
listOfHandlers(“Error: Current speed is greater than the max speed!”);
}
} else {
CurrentSpeed += delta;
Console.WriteLine(“Current speed is : {0}”, CurrentSpeed);
if (MaxSpeed - CurrentSpeed <= 10 && listOfHandlers != null)
{
listOfHandlers(“Warning: Current speed is closing to the max speed!”);
}
}
}
}
class Program
{
public static void OnCarEngineEvent(string message)
{
Console.WriteLine(“=> {0}”, message);
}
static void Main(string[] args)
{
Car c = new Car(“Test”, 100, 10);
c.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
for (int i = 0; i < 6; ++i)
{
c.Accelerate(20);
}
Console.ReadLine();
}
}
}“`
輸出:

Current speed is : 30
Current speed is : 50
Current speed is : 70
Current speed is : 90
=> Warning: Current speed is closing to the max speed!
Current speed is : 110
=> Warning: Current speed is closing to the max speed!
=> Error: Current speed is greater than the max speed!

3. 委託的基礎知識

(1) 委託所實現的功能與C/C++中的函式指標十分相似。Using a delegate allows the programmer to encapsulate a reference to a method inside a delegate object. 從實際使用的角度來看,委託的作用是將方法作為方法的引數。
(2) 與C/C++中函式指標的不同:函式指標只能指向靜態函式,而委託既可以引用靜態函式,又可以引用非靜態成員函式;與函式指標相比,委託是面向物件、型別安全、可靠的受控(managed)物件。
(3) 委託的宣告。Delegates run under the caller’s security permissions, not the declarer’s permissions.
(4) 委託的例項化。委託可以像類一樣直接定義物件,也可以通過關鍵字new建立新的物件。
(5) 將方法繫結到委託。可以直接採用賦值符號=,也可以在new的時候將方法名作為建立委託物件的引數,但與類不同的是a)委託物件一旦建立就要繫結方法,不能建立空的委託物件,b)委託可以通過+=來繫結多個方法,還可以通過-=來解除對某個方法的繫結。
(6) 對於綁定了多個方法的委託,在呼叫時會依次呼叫所有繫結的方法。一旦出現異常會終止方法列表中後面的方法的呼叫。

參考文獻: