1、多播委托
2、事件
3、自定義事件
在上一章中,所有委托都只支持單一回調。
然而,一個委托變量可以引用一系列委托,在這一系列委托中,每個委托都順序指向一個后續的委托,
從而形成了一個委托鏈,或者稱為多播委托*multicast delegate)。
使用多播委托,可以通過一個方法對象來調用一個方法鏈,創建變量來引用方法鏈,並將那些數據類型用
作參數傳遞給方法。
在C#中,多播委托的實現是一個通用的模式,目的是避免大量的手工編碼。這個模式稱為
observer(觀察者)或者publish-subscribe模式,它要應對的是這樣一種情形:你需要將單一事件的通知
(比如對象狀態發生的一個變化)廣播給多個訂閱者(subscriber)。
一、使用多播委托來編碼Observer模式
來考慮一個溫度控制的例子。
假設:一個加熱器和一個冷卻器連接到同一個自動調溫器。
為了控制加熱器和冷卻器的打開和關閉,要向它們通知溫度的變化。
自動調溫器將溫度的變化發布給多個訂閱者---也就是加熱器和冷卻器。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //連接發布者和訂閱者
6 Thermostat tm = new Thermostat(); 7 Cooler cl = new Cooler(40); 8 Heater ht = new Heater(60); 9 //設置委托變量關聯的方法。+=可以存儲多個方法,這些方法稱為訂閱者。
10 tm.OnTemperatureChange += cl.OnTemperatureChanged; 11 tm.OnTemperatureChange += ht.OnTemperatureChanged; 12 string temperature = Console.ReadLine(); 13
14 //將數據發布給訂閱者(本質是依次運行那些方法)
15 tm.OnTemperatureChange(float.Parse(temperature)); 16
17 Console.ReadLine(); 18
19
20
21 } 22 } 23 //兩個訂閱者類
24 class Cooler 25 { 26 public Cooler(float temperature) 27 { 28 _Temperature = temperature; 29 } 30 private float _Temperature; 31 public float Temperature 32 { 33 set
34 { 35 _Temperature = value; 36 } 37 get
38 { 39 return _Temperature; 40 } 41 } 42
43 //將來會用作委托變量使用,也稱為訂閱者方法
44 public void OnTemperatureChanged(float newTemperature) 45 { 46 if (newTemperature > _Temperature) 47 { 48 Console.WriteLine("Cooler:on ! "); 49 } 50 else
51 { 52 Console.WriteLine("Cooler:off ! "); 53 } 54 } 55 } 56 class Heater 57 { 58 public Heater(float temperature) 59 { 60 _Temperature = temperature; 61 } 62 private float _Temperature; 63 public float Temperature 64 { 65 set
66 { 67 _Temperature = value; 68 } 69 get
70 { 71 return _Temperature; 72 } 73 } 74 public void OnTemperatureChanged(float newTemperature) 75 { 76 if (newTemperature < _Temperature) 77 { 78 Console.WriteLine("Heater:on ! "); 79 } 80 else
81 { 82 Console.WriteLine("Heater:off ! "); 83 } 84 } 85 } 86
87
88 //發布者
89 class Thermostat 90 { 91
92 //定義一個委托類型
93 public delegate void TemperatureChangeHanlder(float newTemperature); 94 //定義一個委托類型變量,用來存儲訂閱者列表。注:只需一個委托字段就可以存儲所有訂閱者。
95 private TemperatureChangeHanlder _OnTemperatureChange; 96 //現在的溫度
97 private float _CurrentTemperature; 98
99 public TemperatureChangeHanlder OnTemperatureChange 100 { 101 set { _OnTemperatureChange = value; } 102 get { return _OnTemperatureChange; } 103 } 104
105
106 public float CurrentTemperature 107 { 108 get { return _CurrentTemperature;} 109 set
110 { 111 if (value != _CurrentTemperature) 112 { 113 _CurrentTemperature = value; 114 } 115 } 116 } 117 }
上述代碼使用+=運算符來直接賦值。向其OnTemperatureChange委托注冊了兩個訂閱者。
目前還沒有將發布Thermostat類的CurrentTemperature屬性每次變化時的值,通過調用委托來
向訂閱者通知溫度的變化,為此需要修改屬性的set語句。
這樣以后,每次溫度變化都會通知兩個訂閱者。
public float CurrentTemperature { get { return _CurrentTemperature; } set { if (value != _CurrentTemperature) { _CurrentTemperature = value; OnTemperatureChange(value); } } }
這里,只需要執行一個調用,即可向多個訂閱者發出通知----這天是將委托更明確地
稱為“多播委托”的原因。
針對這種以上的寫法有幾個需要注意的點:
1、在發布事件代碼時非常重要的一個步驟:假如當前沒有訂閱者注冊接收通知。
則OnTemperatureChange為空,執行OnTemperatureChange(value)語句會引發一
個NullReferenceException。所以需要檢查空值。
public float CurrentTemperature { get { return _CurrentTemperature; } set { if (value != _CurrentTemperature) { _CurrentTemperature = value; TemperatureChangeHanlder localOnChange = OnTemperatureChange; if (localOnChange != null) { //OnTemperatureChange = null;
localOnChange(value); } } } }
在這里,我們並不是一開始就檢查空值,而是首先將OnTemperatureChange賦值給另一個委托變量localOnChange .
這個簡單的修改可以確保在檢查空值和發送通知之間,假如所有OnTemperatureChange訂閱者都被移除(由一個不同的線程),那么不會觸發
NullReferenceException異常。
注:將-=運算符應用於委托會返回一個新實例。
對委托OnTemperatureChange-=訂閱者,的任何調用都不會從OnTemperatureChange中刪除一個委托而使它的委托比之前少一個,相反,
會將一個全新的多播委托指派給它,這不會對原始的多播委托產生任何影響(localOnChange也指向那個原始的多播委托),只會減少對它的一個引用。
委托是一個引用類型。
2、委托運算符
為了合並Thermostat例子中的兩個訂閱者,要使用"+="運算符。
這樣會獲取引一個委托,並將第二個委托添加到委托鏈中,使一個委托指向下一個委托。
第一個委托的方法被調用之后,它會調用第二個委托。從委托鏈中刪除委托,則要使用"-="運算符。
1 Thermostat.TemperatureChangeHanlder delegate1; 2 Thermostat.TemperatureChangeHanlder delegate2; 3 Thermostat.TemperatureChangeHanlder delegate3; 4 delegate3 = tm.OnTemperatureChange; 5 delegate1 = cl.OnTemperatureChanged; 6 delegate2 = ht.OnTemperatureChanged; 7 delegate3 += delegate1; 8 delegate3 += delegate2;
同理可以使用+ 與 - 。
1 Thermostat.TemperatureChangeHanlder delegate1; 2 Thermostat.TemperatureChangeHanlder delegate2; 3 Thermostat.TemperatureChangeHanlder delegate3; 4 delegate1 = cl.OnTemperatureChanged; 5 delegate2 = ht.OnTemperatureChanged; 6 delegate3 = delegate1 + delegate2; 7 delegate3 = delegate3 - delegate2; 8 tm.OnTemperatureChange = delegate3;
使用賦值運算符,會清除之前的所有訂閱者,並允許使用新的訂閱者替換它們。
這是委托很容易讓人犯錯的一個設置。因為本來需要使用"+="運算的時候,很容易就會錯誤地寫成"="
無論是 +、-、 +=、 -=,在內部都是使用靜態方法System.Delegate.Combine()和System.Delegate.Remove()來實現的。
3、順序調用
委托調用順序圖,需要下載。
雖然一個tm.OnTemperatureChange()調用造成每個訂閱者都收到通知,但它們仍然是順序調用的,而不是同時調用,因為
一個委托能指向另一個委托,后者又能指向其它委托。
注:多播委托的內部機制
delegate關鍵字是派生自System.MulticastDelegate的一個類型的別名。
System.MulticastDelegate則是從System.Delegate派生的,后者由一個對象引用和一個System.Reflection.MethodInfo類型的該批針構成。
創建一個委托時,編譯器自動使用System.MulticastDelegate類型而不是System.Delegate類型。
MulticastDelegate類包含一個對象引用和一個方法指針,這和它的Delegate基類是一樣的,但除此之外,
它還包含對另一個System.MulticastDelegate對象的引用 。
向一個多播委托添加一個方法時,MulticastDelegate類會創建委托類型的一個新實例,在新實例中為新增的方法存儲對象引用和方法指針,
並在委托實例列表中添加新的委托實例作為下一項。
這樣的結果就是,MulticastDelegate類維護關由多個Delegate對象構成的一個鏈表。
調用多播委托時,鏈表中的委托實例會被順序調用。通常,委托是按照它們添加時的順序調用的。
4、錯誤處理
錯誤處理凸顯了順序通知的重要性。假如一個訂閱者引發一個異常,鏈中后續訂閱不接收不到通知。
為了避免這個問題,使所有訂閱者都能收到通知,必須手動遍歷訂閱者列表,並單獨調用它們。
1 public float CurrentTemperature 2 { 3 get { return _CurrentTemperature; } 4 set
5 { 6 if (value != _CurrentTemperature) 7 { 8
9 _CurrentTemperature = value; 10 TemperatureChangeHanlder localOnChange = OnTemperatureChange; 11 if (localOnChange != null) 12 { 13 foreach (TemperatureChangeHanlder hanlder in localOnChange.GetInvocationList()) 14 { 15 try
16 { 17 hanlder(value); 18 } 19 catch (Exception e) 20 { 21 Console.WriteLine(e.Message); 22
23 } 24 } 25 } 26
27 } 28 } 29 }
5、方法返回值和傳引用
在這種情形下,也有必要遍歷委托調用列表,而非直接激活一個通知。
因為不同的訂閱者返回的值可能不一。所以需要單獨獲取。
二、事件
目前使用的委托存在兩個關鍵的問題。C#使用關鍵字event(事件)一解決這些問題。
二、1 事件的作用:
1、封裝訂閱
如前所述,可以使用賦值運算符將一個委托賦給另一個。但這有可能造成bug。
在本應該使用 "+=" 的位置,使用了"="。為了防止這種錯誤,就是根本
不為包容類外部的對象提供對賦值運算符的運行。event關鍵字的目的就是提供額外
的封裝,避免你不小心地取消其它訂閱者。
2、封裝發布
委托和事件的第二個重要區別在於,事件確保只有包容類才能觸發一個事件通知。防止在包容
類外部調用發布者發布事件通知。
禁止如以下的代碼:
tm.OnTemperatureChange(100);
即使tm的CurrentTemperature沒有發生改變,也能調用tm.OnTemperatureChange委托。
所以和訂閱者一樣,委托的問題在於封裝不充分。
二、2 事件的聲明
C#用event關鍵字解決了上述兩個問題,雖然看起來像是一個字段修飾符,但event定義的是一個新的成員類型。
1 public class Thermostat 2 { 3 private float _CurrentTemperature; 4 public float CurrentTemperature 5 { 6 set { _CurrentTemperature = value; } 7 get { return _CurrentTemperature; } 8 } 9 //定義委托類型
10 public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); 11
12 //定義一個委托變量,並用event修飾,被修飾后有一個新的名字,事件發布者。
13 public event TemperatureChangeHandler OnTemperatureChange = delegate { }; 14
15
16 public class TemperatureArgs : System.EventArgs 17 { 18 private float _newTemperature; 19 public float NewTemperature 20 { 21 set { _newTemperature = value; } 22 get { return _newTemperature; } 23 } 24 public TemperatureArgs(float newTemperature) 25 { 26 _newTemperature = newTemperature; 27 } 28
29 } 30 }
這個新的Thermostat類進行了幾處修改:
a、OnTemperatureChange屬性被移除了,且被聲明為一個public字段
b、在OnTemperatureChange聲明為字段的同時,使用了event關鍵字,這會禁止為一個public委托字段使用賦值運算符。
只有包容類才能調用向所有訂閱者發布通知的委托。
以上兩點解決了委托普通存在 的兩個問題
c、普通委托的另一個不利之處在於,易忘記在調用委托之前檢查null值,
通過event關鍵字提供的封裝,可以在聲明(或者在構造器中)采用一個替代方案,以上代碼賦值了空委托。
當然,如果委托存在被重新賦值為null的任何可能,仍需要進行null值檢查。
d、委托類型發生了改變,將原來的單個temperature參數替換成兩個新參數。
二、3 編碼規范
在以上的代碼中,委托聲明還發生另一處修改。
為了遵循標准的C#編碼規范,修改了TemperatureChangeHandler,將原來的單個temperature參數替換成兩新參數,
即sender和temperatureArgs。這一處修改並不是C#編譯器強制的。
但是,聲明一個打算作為事件來使用的委托時,規范要求你傳遞這些類型的兩個參數。
第一個參數sender就包含"調用委托的那個類"的一個實例。假如一個訂閱者方法注冊了多個事件,這個參數就尤其有用。
如兩個不同的Thermostata實例都訂閱了heater.OnTemperatureChanged事件,在這種情況下,任何一個Thermostat實例都
可能觸發對heater.OnTemperatureChanged的一個調用,為了判斷具體是哪一個Thermostat實例觸發了事件,要在Heater.OnTemperatureChanged()
內部利用sender參數進行判斷。
第二個參數temperatureArgs屬性Thermostat.TemperatureArgs類型。在這里使用嵌套類是恰當的,因為它遵循和OntermperatureChangeHandler委托本身
相同的作用域。
Thermostat.TemperatureArgs,一個重點在於它是從System.EventArgs派生的。System.EventArgs唯一重要的屬性是
Empty,它指出不存在事件數據。然而,從System.EventArgs派生出TemperatureArgs時,你添加了一個額外的屬性,名為NewTemperature。這樣一來
就可以將溫度從自動調溫器傳遞到訂閱者那里。
編碼規范小結:
1、第一個參數sender是object類型的,它包含對調用委托的那個對象的一個引用。
2、第二個參數是System.EventArgs類型的(或者是從System.EventArgs派生,但包含了事件數據的其它類型。)
調用委托的方式和以前幾乎完全一樣,只是要提供附加的參數。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Thermostat tm = new Thermostat(); 6
7 Cooler cl = new Cooler(40); 8 Heater ht = new Heater(60); 9
10 //設置訂閱者(方法)
11 tm.OnTemperatureChange += cl.OnTemperatureChanged; 12 tm.OnTemperatureChange += ht.OnTemperatureChanged; 13
14 tm.CurrentTemperature = 100; 15 } 16 } 17 //發布者類
18 public class Thermostat 19 { 20 private float _CurrentTemperature; 21 public float CurrentTemperature 22 { 23 set
24 { 25 if (value != _CurrentTemperature) 26 { 27 _CurrentTemperature = value; 28 if (OnTemperatureChange != null) 29 { 30 OnTemperatureChange(this, new TemperatureArgs(value)); 31 } 32
33 } 34 } 35 get { return _CurrentTemperature; } 36 } 37 //定義委托類型
38 public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); 39
40 //定義一個委托變量,並用event修飾,被修飾后有一個新的名字,事件發布者。
41 public event TemperatureChangeHandler OnTemperatureChange = delegate { }; 42
43 //用來給事件傳遞的數據類型
44 public class TemperatureArgs : System.EventArgs 45 { 46 private float _newTemperature; 47 public float NewTemperature 48 { 49 set { _newTemperature = value; } 50 get { return _newTemperature; } 51 } 52 public TemperatureArgs(float newTemperature) 53 { 54 _newTemperature = newTemperature; 55 } 56
57 } 58 } 59
60 //兩個訂閱者類
61 class Cooler 62 { 63 public Cooler(float temperature) 64 { 65 _Temperature = temperature; 66 } 67 private float _Temperature; 68 public float Temperature 69 { 70 set
71 { 72 _Temperature = value; 73 } 74 get
75 { 76 return _Temperature; 77 } 78 } 79
80 //將來會用作委托變量使用,也稱為訂閱者方法
81 public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature) 82 { 83 if (newTemperature.NewTemperature > _Temperature) 84 { 85 Console.WriteLine("Cooler:on ! "); 86 } 87 else
88 { 89 Console.WriteLine("Cooler:off ! "); 90 } 91 } 92 } 93 class Heater 94 { 95 public Heater(float temperature) 96 { 97 _Temperature = temperature; 98 } 99 private float _Temperature; 100 public float Temperature 101 { 102 set
103 { 104 _Temperature = value; 105 } 106 get
107 { 108 return _Temperature; 109 } 110 } 111 public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature) 112 { 113 if (newTemperature.NewTemperature < _Temperature) 114 { 115 Console.WriteLine("Heater:on ! "); 116 } 117 else
118 { 119 Console.WriteLine("Heater:off ! "); 120 } 121 } 122 }
通過將sender指定為容器類(this),因為它是能為事件調用委托的唯一一個類。
在這個例子中,訂閱者可以將sender參數強制轉型為Thermostat,並以那種方式來訪問當前溫度,
或通過TemperatureArgs實例來訪問在。
然而,Thermostat實例上的當前溫度可能由一個不同的線程改變。
在由於狀態改變而發生事件的時候,連同新值傳遞前一個值是一個常見的編程模式,它可以決定哪些狀態變化是
允許的。
二、4 泛型和委托
使用泛型,可以在多個位置使用相同的委托數據類型,並在支持多個不同的參數類型的同時保持強類型。
在C#2.0和更高版本需要使用事件的大多數場合中,都無需要聲明一個自定義的委托數據類型
System.EventHandler<T> 已經包含在Framework Class Library
注:System.EventHandler<T> 用一個約束來限制T從EventArgs派生。注意是為了向上兼容。
//定義委托類型
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue);
//定義一個委托變量,並用event修飾,被修飾后有一個新的名字,事件發布者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { };
使用以下泛型代替:
public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { };
事件的內部機制:
事件是限制外部類只能通過 "+="運算符向發布添加訂閱方法,並用"-="運算符取消訂閱,除此之外的任何事件都不允許做。
此外,它們還阻止除包容類之外的其他任何類調用事件。
為了達到上述目的,C#編譯器會獲取帶有event修飾符的public委托變量,並將委托聲明為private。
除此之外,它還添加了兩個方法和兩個特殊的事件塊。從本質上說,event關鍵字是編譯器用於生成恰當封裝邏輯的
一個C#快捷方式。
C#實在現一個屬性時,會創建get set,
此處的事件屬性使用了 add remove分別使用了Sytem.Delegate.Combine
與 System.Delegate.Remove
1 //定義委托類型
2 public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); 3
4 //定義一個委托變量,並用event修飾,被修飾后有一個新的名字,事件發布者。
5 public event TemperatureChangeHandler OnTemperatureChange = delegate { }; 6
7 在編譯器的作用下,會自動擴展成: 8 private TemperatureChangeHandler _OnTemperatureChange = delegate { }; 9
10 public void add_OnTemperatureChange(TemperatureChangeHandler handler) 11 { 12 Delegate.Combine(_OnTemperatureChange, handler); 13 } 14 public void remove_OnTemperatureChange(TemperatureChangeHandler handler) 15 { 16 Delegate.Remove(_OnTemperatureChange, handler); 17 } 18 public event TemperatureChangeHandler OnTemperatureChange 19 { 20 add 21 { 22 add_OnTemperatureChange(value); 23 } 24
25 remove 26 { 27 remove_OnTemperatureChange(value); 28 } 29
30 }
這兩個方法add_OnTemperatureChange與remove_OnTemperatureChange 分別負責實現
"+="和"-="賦值運算符。
在最終的CIL代碼中,仍然保留了event關鍵字。
換言之,事件是CIL代碼能夠顯式識別的一樣東西,它並非只是一個C#構造。
二、5 自定義事件實現
編譯器為"+="和"-="生成的代碼是可以自定義的。
例如,將OnTemperatureChange委托的作用域改成protected而不是private。這樣一來,從Thermostat派生的類就被允許直接訪問委托,
而無需受到和外部類一樣的限制。為此,可以允許添加定制的add 和 remove塊。
1 protected TemperatureChangeHandler _OnTemperatureChange = delegate { }; 2
3 public event TemperatureChangeHandler OnTemperatureChange 4 { 5 add 6 { 7 //此處代碼可以自定義
8 Delegate.Combine(_OnTemperatureChange, value); 9
10 } 11
12 remove 13 { 14 //此處代碼可以自定義
15 Delegate.Remove(_OnTemperatureChange, value); 16 } 17
18 }
以后繼承這個類的子類,就可以重寫這個屬性了。
實現自定義事件。
小結:通常,方法指針是唯一需要在事件上下文的外部乃至委托變量情況。
換句話說:由於事件提供了額外的封裝特性,而且允許你在必要時對實現進行自定義,所以最佳
做法就是始終為Observer模式使用事件。