前言
委託類型的實例是存儲着一個方法,並通過委託來調用那個方法,但是委託還有其他的用途。
先講一個模式:publish-subscribe(訂閱-發佈)
它是應對這樣的一個場景情形:就是把單一事件的通知廣播給多個訂閱者。
這句話通俗一點講的話就是:
現在有方法A、B、C、D、E,自己想調用這五個方法中的全部或者部分方法。但是又不想一個一個顯式的去調用,因爲如果方法很多的話就會形成一個代碼的堆砌,不夠簡潔,時間一長也不好維護。這時候有一個想法就是能有一個“方法F”來收集自己想要調用的方法就好了,最後自己只要調用“方法F”就可以調用所有自己想要調用的方法。
到這邊有人可能就感覺不是很自然,感覺有點陌生,沒有關係,我也是和你一樣很陌生,本文就是幫助你對publish-subscribe(訂閱-發佈)這個模式的熟悉並且完全掌握。
好的,publish-subscribe(訂閱-發佈)先講到這邊。本文的第一句話就提到委託還有其他的用途。具體是指:一個委託變量可以引用一系列委託,在這一系列委託中,每一個委託都會順序指向一個後續的委託,從而形成一個委託鏈。只要調用這個委託的方法對象,在這個委託鏈上的所有方法就會按照委託鏈的順序一一執行。我們在這邊可以做一個猜想:委託變量可以調用多個方法,是不是“方法F”就是用委託變量來實現的呢? 其實答案就是這樣。
具體的場景描述:
來考慮一個溫度控制的例子。一個加熱器和一個冷卻器連接到同一個自動調溫器。爲了控制加熱器和冷卻器的打開和關閉,要向他們通知溫度的變化。自動調溫器將溫度的變化發佈給多個訂閱者----也就是加熱器和冷卻器。
一、定義訂閱者的方法
public class Cooler
{
public Cooler(float temperature)
{
Temperature = temperature;
}
public float Temperature { get; set; }
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature > Temperature)
{
Console.WriteLine("Cooler:ON");
}
else
{
Console.WriteLine("Cooler:Off");
}
}
}
public class Heater
{
public Heater(float temperature)
{
Temperature = temperature;
}
public float Temperature { get; set; }
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < Temperature)
{
Console.WriteLine("Heater:ON");
}
else
{
Console.WriteLine("Heater:Off");
}
}
}
這兩個類幾乎完全一致,兩個類都提供了一個OnTemperatureChanged方法,調用OnTemperatureChanged就是爲了向Heater和Cooler指出溫度變化,並決定是否讓設備啓動。在這裏,兩個OnTemperatureChanged方法都是訂閱者方法。作爲訂閱者方法很重要的是它們的參數和返回值類型必須和自動調溫器中的委託匹配。
二、定義發佈者
Thermostat類負責向heater和cooler對象實例報告溫度的變化。
public class Thermostat
{
public delegate void TemperarureChangeHandler(float newTemperature);
public TemperarureChangeHandler OnTemperatureChange
{ get; set; }
public float CurrentTemperature//接受當前的溫度
{ get; set; }
}
這個類的第一個成員是TemperarureChangeHandler 委託。定義了訂閱者的方法類型,就是說在Heater和Cooler類中的OnTemperatureChanged成員方法和TemperarureChangeHandler委託是匹配的。OnTemperatureChange成員屬性是TemperarureChangeHandler委託類型的,將會用來存儲着訂閱者列表。最後一個CurrentTemperature屬性是用來接收當前的溫度的。
三、連接發佈者和訂閱者
public class Program
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temerature;
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
Console.Write("Enter temperature:");
temerature = Console.ReadLine();
thermostat.CurrentTemperature = int.Parse(temerature);
}
注意上述代碼使用+=運算符來直接賦值,向OnTemperatureChange委託註冊了兩個訂閱者。但是目前還沒有寫任何代碼將溫度變化發佈給訂閱者。
四、調用委託,向訂閱者通知溫度的變化
public class Thermostat
{
…
…
public float CurrentTemperature//當CurrentTemperature屬性每次變化的時候調用委託來向訂閱者通知溫度的變化
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature=value;
OnTemperatureChange(value);
}
}
}
private float _CurrentTemperature;
}
成功了成功了,現在對CurrentTemperature賦值包含了一些特殊的邏輯,可以向訂閱者通知CurrentTemperature發生了變化。爲了向所有的訂閱者發出通知,只需執行一個簡單的C# 語句即:OnTemperatureChange(value); 這個語句將溫度的變化發給Cooler和Heater的對象,只需執行一個調用,即可向多個訂閱者發出通知。這裏的實現就是基於一個委託變量可以保存一個委託鏈。
五、在調用委託之前必須檢查委託對象是否爲空
public class Thermostat
{
…
…
public float CurrentTemperature//當CurrentTemperature屬性每次變化的時候調用委託檢查空值
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
TemperarureChangeHandler localOnChange = OnTemperatureChange;//OnTemperatureChange中發生的任何改變都會在localOnChange中反映出來
if (localOnChange != null)
{
localOnChange(value);
}
}
}
}
private float _CurrentTemperature;
}
在這裏,並不是一開始就檢查空值,而是首先是將OnTemperatureChange賦值給 localOnChange因爲OnTemperatureChange 的訂閱者被不是同一個線程的方法移除時候,那麼不會觸發NullReferenceException異常。
六、處理來自訂閱者的異常
public class Thermostat
{
…
…
public float CurrentTemperature//當CurrentTemperature屬性每次變化的時候調用委託並處理來自訂閱者的異常
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
//OnTemperatureChange中發生的任何改變都會在localOnChange中反映出來TemperarureChangeHandler localOnChange = OnTemperatureChange;
if (localOnChange != null)
{
//防止因爲訂閱者的異常導致在異常後面的訂閱者不能接收到發佈事件foreach (TemperarureChangeHandler handler in localOnChange.GetInvocationList())
{
try
{
handler(value);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
}
}
}
}
private float _CurrentTemperature;
}
委託鏈是將多個方法串聯在一起的,假如一個委託鏈上有A、B、C三個註冊的方法,但是假如A中出現異常,B和C是不會繼續執行的,那麼解決的辦法就是用foreach 上面的代碼就是解決方案。
七、事件的出現
講了這麼多的還是沒有講到事件,哈哈,是不是被耍了啊。不要急,一會就會講到事件了啊。
我們看上面的委託處理還是很不錯的,不過有兩點不是很好
1、 在Main方法中給委託註冊事件
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
假設我們不小心的把+=寫成了=,就會出現我們不想要的結果,就會在thermostat.OnTemperatureChange 中保存最後一個賦值的方法,而之前的都會被重寫,容易出錯。
2、 委託可以在包容類之外被執行調用
還是在Main方法中可以加上這句話:
thermostat.OnTemperatureChange(56);
加上這句話會導致的結果是:即使thermostat的CurrentTemperature沒有發生變化,OnTemperatureChange也能被調用。因此thermostat訂閱者有可能被通知說溫度變化了,而實際上CurrentTemperature的溫度並沒有變化。
C# 用event 關鍵字解決上面的兩個問題。代碼:
public class Thermostat
{
public class TemperatureArgs : System.EventArgs
{
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
public float NewTemperature
{ get; set; }
}
//public delegate void TemperarureChangeHandler(float newTemperature);
public delegate void TemperarureChangeHandler(object sender,TemperatureArgs newTemperature);
public event TemperarureChangeHandler OnTemperatureChange = delegate { };
public float CurrentTemperature//當CurrentTemperature屬性每次變化的時候調用委託並處理來自訂閱者的異常
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
TemperarureChangeHandler localOnChange = OnTemperatureChange;//OnTemperatureChange中發生的任何改變都會在localOnChange中反映出來
if (localOnChange != null)
{
foreach (TemperarureChangeHandler handler in localOnChange.GetInvocationList())//防止因爲訂閱者的異常導致在異常後面的訂閱者不能接收到發佈事件
{
try
{
handler(this,new TemperatureArgs(value));
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
}
}
}
}
private float _CurrentTemperature;
}
有了這樣的聲明,在加上event關鍵字後,提供了我們需要的全部封裝。首先會禁止使用賦值運算符,然後只有包容類才能調用向所有的訂閱者發出通知的委託。