標籤:
C# 事件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 OnTemperatureChange100 {101 set { _OnTemperatureChange = value; }102 get { return _OnTemperatureChange; }103 }104 105 106 public float CurrentTemperature107 {108 get { return _CurrentTemperature;}109 set110 {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 try16 {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.EventArgs17 {18 private float _newTemperature;19 public float NewTemperature20 {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 Temperature101 {102 set103 {104 _Temperature = value;105 }106 get107 {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 else118 {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 OnTemperatureChange19 {20 add21 {22 add_OnTemperatureChange(value);23 }24 25 remove26 {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 remove13 {14 //此處代碼可以自訂15 Delegate.Remove(_OnTemperatureChange, value);16 }17 18 }
以後繼承這個類的子類,就可以重寫這個屬性了。實現自訂事件。 小結:通常,方法指標是唯一需要在事件內容相關的外部乃至委託變數情況。換句話說:由於事件提供了額外的封裝特性,而且允許你在必要時對實現進行自訂,所以最佳做法就是始終為Observer模式使用事件。
C# 事件