C# 事件

來源:互聯網
上載者:User

標籤:

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# 事件

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.