1、多播委託
2、事件
3、自定義事件
在上一章中,所有委託都只支援單一回調。
然而,一個委託變數可以引用一系列委託,在這一系列委託中,每個委託都順序指向一個後續的委託,
從而形成了一個委託鏈,或者稱為多播委託*multicast delegate)。
使用多播委託,可以通過一個方法物件來呼叫一個方法鏈,建立變數來引用方法鏈,並將那些資料型別用
作引數傳遞給方法。
在C#中,多播委託的實現是一個通用的模式,目的是避免大量的手工編碼。這個模式稱為
observer(觀察者)或者publish-subscribe模式,它要應對的是這樣一種情形:你需要將單一事件的通知
(比如物件狀態發生的一個變化)廣播給多個訂閱者(subscriber)。
一、使用多播委託來編碼Observer模式
來考慮一個溫度控制的例子。
假設:一個加熱器和一個冷卻器連線到同一個自動調溫器。
為了控制加熱器和冷卻器的開啟和關閉,要向它們通知溫度的變化。
自動調溫器將溫度的變化釋出給多個訂閱者---也就是加熱器和冷卻器。
class Program
{
static void Main(string[] args)
{
//連線釋出者和訂閱者
Thermostat tm = new Thermostat();
Cooler cl = new Cooler();
Heater ht = new Heater();
//設定委託變數關聯的方法。+=可以儲存多個方法,這些方法稱為訂閱者。
tm.OnTemperatureChange += cl.OnTemperatureChanged;
tm.OnTemperatureChange += ht.OnTemperatureChanged;
string temperature = Console.ReadLine(); //將資料釋出給訂閱者(本質是依次執行那些方法)
tm.OnTemperatureChange(float.Parse(temperature)); Console.ReadLine(); }
}
//兩個訂閱者類
class Cooler
{
public Cooler(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
} //將來會用作委託變數使用,也稱為訂閱者方法
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature > _Temperature)
{
Console.WriteLine("Cooler:on ! ");
}
else
{
Console.WriteLine("Cooler:off ! ");
}
}
}
class Heater
{
public Heater(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < _Temperature)
{
Console.WriteLine("Heater:on ! ");
}
else
{
Console.WriteLine("Heater:off ! ");
}
}
} //釋出者
class Thermostat
{ //定義一個委託型別
public delegate void TemperatureChangeHanlder(float newTemperature);
//定義一個委託型別變數,用來儲存訂閱者列表。注:只需一個委託欄位就可以儲存所有訂閱者。
private TemperatureChangeHanlder _OnTemperatureChange;
//現在的溫度
private float _CurrentTemperature; public TemperatureChangeHanlder OnTemperatureChange
{
set { _OnTemperatureChange = value; }
get { return _OnTemperatureChange; }
} public float CurrentTemperature
{
get { return _CurrentTemperature;}
set
{
if (value != _CurrentTemperature)
{
_CurrentTemperature = value;
}
}
}
}
上述程式碼使用+=運算子來直接賦值。向其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例子中的兩個訂閱者,要使用"+="運算子。
這樣會獲取引一個委託,並將第二個委託新增到委託鏈中,使一個委託指向下一個委託。
第一個委託的方法被呼叫之後,它會呼叫第二個委託。從委託鏈中刪除委託,則要使用"-="運算子。
Thermostat.TemperatureChangeHanlder delegate1;
Thermostat.TemperatureChangeHanlder delegate2;
Thermostat.TemperatureChangeHanlder delegate3;
delegate3 = tm.OnTemperatureChange;
delegate1 = cl.OnTemperatureChanged;
delegate2 = ht.OnTemperatureChanged;
delegate3 += delegate1;
delegate3 += delegate2;
同理可以使用+ 與 - 。
Thermostat.TemperatureChangeHanlder delegate1;
Thermostat.TemperatureChangeHanlder delegate2;
Thermostat.TemperatureChangeHanlder delegate3;
delegate1 = cl.OnTemperatureChanged;
delegate2 = ht.OnTemperatureChanged;
delegate3 = delegate1 + delegate2;
delegate3 = delegate3 - delegate2;
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、錯誤處理
錯誤處理凸顯了順序通知的重要性。假如一個訂閱者引發一個異常,鏈中後續訂閱不接收不到通知。
為了避免這個問題,使所有訂閱者都能收到通知,必須手動遍歷訂閱者列表,並單獨呼叫它們。
public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if (value != _CurrentTemperature)
{ _CurrentTemperature = value;
TemperatureChangeHanlder localOnChange = OnTemperatureChange;
if (localOnChange != null)
{
foreach (TemperatureChangeHanlder hanlder in localOnChange.GetInvocationList())
{
try
{
hanlder(value);
}
catch (Exception e)
{
Console.WriteLine(e.Message); }
}
} }
}
}
5、方法返回值和傳引用
在這種情形下,也有必要遍歷委託呼叫列表,而非直接啟用一個通知。
因為不同的訂閱者返回的值可能不一。所以需要單獨獲取。
二、事件
目前使用的委託存在兩個關鍵的問題。C#使用關鍵字event(事件)一解決這些問題。
二、1 事件的作用:
1、封裝訂閱
如前所述,可以使用賦值運算子將一個委託賦給另一個。但這有可能造成bug。
在本應該使用 "+=" 的位置,使用了"="。為了防止這種錯誤,就是根本
不為包容類外部的物件提供對賦值運算子的執行。event關鍵字的目的就是提供額外
的封裝,避免你不小心地取消其它訂閱者。
2、封裝釋出
委託和事件的第二個重要區別在於,事件確保只有包容類才能觸發一個事件通知。防止在包容
類外部呼叫釋出者釋出事件通知。
禁止如以下的程式碼:
tm.OnTemperatureChange(100);
即使tm的CurrentTemperature沒有發生改變,也能呼叫tm.OnTemperatureChange委託。
所以和訂閱者一樣,委託的問題在於封裝不充分。
二、2 事件的宣告
C#用event關鍵字解決了上述兩個問題,雖然看起來像是一個欄位修飾符,但event定義的是一個新的成員型別。
public class Thermostat
{
private float _CurrentTemperature;
public float CurrentTemperature
{
set { _CurrentTemperature = value; }
get { return _CurrentTemperature; }
}
//定義委託型別
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定義一個委託變數,並用event修飾,被修飾後有一個新的名字,事件釋出者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; public class TemperatureArgs : System.EventArgs
{
private float _newTemperature;
public float NewTemperature
{
set { _newTemperature = value; }
get { return _newTemperature; }
}
public TemperatureArgs(float newTemperature)
{
_newTemperature = newTemperature;
} }
}
這個新的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派生,但包含了事件資料的其它型別。)
呼叫委託的方式和以前幾乎完全一樣,只是要提供附加的引數。
class Program
{
static void Main(string[] args)
{
Thermostat tm = new Thermostat(); Cooler cl = new Cooler();
Heater ht = new Heater(); //設定訂閱者(方法)
tm.OnTemperatureChange += cl.OnTemperatureChanged;
tm.OnTemperatureChange += ht.OnTemperatureChanged; tm.CurrentTemperature = ;
}
}
//釋出者類
public class Thermostat
{
private float _CurrentTemperature;
public float CurrentTemperature
{
set
{
if (value != _CurrentTemperature)
{
_CurrentTemperature = value;
if (OnTemperatureChange != null)
{
OnTemperatureChange(this, new TemperatureArgs(value));
} }
}
get { return _CurrentTemperature; }
}
//定義委託型別
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定義一個委託變數,並用event修飾,被修飾後有一個新的名字,事件釋出者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; //用來給事件傳遞的資料型別
public class TemperatureArgs : System.EventArgs
{
private float _newTemperature;
public float NewTemperature
{
set { _newTemperature = value; }
get { return _newTemperature; }
}
public TemperatureArgs(float newTemperature)
{
_newTemperature = newTemperature;
} }
} //兩個訂閱者類
class Cooler
{
public Cooler(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
} //將來會用作委託變數使用,也稱為訂閱者方法
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
{
if (newTemperature.NewTemperature > _Temperature)
{
Console.WriteLine("Cooler:on ! ");
}
else
{
Console.WriteLine("Cooler:off ! ");
}
}
}
class Heater
{
public Heater(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
}
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
{
if (newTemperature.NewTemperature < _Temperature)
{
Console.WriteLine("Heater:on ! ");
}
else
{
Console.WriteLine("Heater:off ! ");
}
}
}
通過將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
//定義委託型別
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定義一個委託變數,並用event修飾,被修飾後有一個新的名字,事件釋出者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; 在編譯器的作用下,會自動擴充套件成:
private TemperatureChangeHandler _OnTemperatureChange = delegate { }; public void add_OnTemperatureChange(TemperatureChangeHandler handler)
{
Delegate.Combine(_OnTemperatureChange, handler);
}
public void remove_OnTemperatureChange(TemperatureChangeHandler handler)
{
Delegate.Remove(_OnTemperatureChange, handler);
}
public event TemperatureChangeHandler OnTemperatureChange
{
add
{
add_OnTemperatureChange(value);
} remove
{
remove_OnTemperatureChange(value);
} }
這兩個方法add_OnTemperatureChange與remove_OnTemperatureChange 分別負責實現
"+="和"-="賦值運算子。
在最終的CIL程式碼中,仍然保留了event關鍵字。
換言之,事件是CIL程式碼能夠顯式識別的一樣東西,它並非只是一個C#構造。
二、5 自定義事件實現
編譯器為"+="和"-="生成的程式碼是可以自定義的。
例如,將OnTemperatureChange委託的作用域改成protected而不是private。這樣一來,從Thermostat派生的類就被允許直接訪問委託,
而無需受到和外部類一樣的限制。為此,可以允許新增定製的add 和 remove塊。
protected TemperatureChangeHandler _OnTemperatureChange = delegate { }; public event TemperatureChangeHandler OnTemperatureChange
{
add
{
//此處程式碼可以自定義
Delegate.Combine(_OnTemperatureChange, value); } remove
{
//此處程式碼可以自定義
Delegate.Remove(_OnTemperatureChange, value);
} }
以後繼承這個類的子類,就可以重寫這個屬性了。
實現自定義事件。
小結:通常,方法指標是唯一需要在事件上下文的外部乃至委託變數情況。
換句話說:由於事件提供了額外的封裝特性,而且允許你在必要時對實現進行自定義,所以最佳
做法就是始終為Observer模式使用事件。