十三、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 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模式使用事件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM