第 49 講:委托(二):委托的加減法及基類型介紹
沒有辦法,委托是一個非常重要的概念。在你學(xué)習(xí)和使用窗體程序的時候,其實你應(yīng)該知道的是 C# 寫這些東西非常方便。以前的 WinForm、現(xiàn)在的 WPF、UWP、甚至是跨平臺 UI 框架 MAUI,都是 C# 開發(fā)的;而且這些窗體程序的框架,底層代碼大量依賴委托和事件。如果你不學(xué)好這些的話,以后也會寸步難行。所以一定要開動腦筋多思考。
Part 1 委托的加法
委托除了前面指代方法,進行回調(diào)(Callback)的特效以外,委托還有一個特別棒的、前文沒有介紹的特效:一個委托實例并不是只能調(diào)用它回調(diào)的那一個唯一的方法。實際上,一個委托實例可以無限往后追加回調(diào)函數(shù),到時候通過一次 Invoke
調(diào)用,所有方法全部得到調(diào)用。下面我們來演示一下這個例子。
首先,我們預(yù)留 4 個處理不相同的方法,它們的簽名是一樣的:無參數(shù)無返回值。
接下來,我們定義一個無參數(shù)無返回值的委托類型:
Handler
+
運算符,然后累計 handler
注意第 2、3、4 行代碼。我們使用 +=
運算符,它等價于 handler = handler + new Handler(方法名)
的語句。我們執(zhí)行三次,這表示把 A
、B
和 D
四個方法全部累計到 handler
委托對象里去。
接著,我們使用 Invoke
對委托對象 handler
進行回調(diào)。
我們開始運行程序,你可以看到,1、2、3、4 會順次輸出到屏幕上。這就是整個程序的執(zhí)行效果。那么我們回頭來說一下整個委托加法運算的意思。
委托加法是將兩個同類型的委托對象通過這個運算符,將里面的回調(diào)函數(shù)歸并一起,形成一個委托對象。比如
這樣的句子。假設(shè) handler1
和 handler2
分別存儲了回調(diào)函數(shù) A
和回調(diào)函數(shù) B
的話,那么最終 handler
這個對象里就存儲了 A
和 B
兩個回調(diào)函數(shù)。那么,一旦我們啟動對 handler
這個整合的委托對象的回調(diào)(即調(diào)用 handler.Invoke()
)的話,那么你就可以按次序看到 A
方法被調(diào)用,然后是 B
方法被調(diào)用。這個就是委托的加法運算,它和字符串拼接是差不多效果的。
另外補充一下。因為是類似字符串拼接,所以是順次拼接進去得到合并結(jié)果,所以原始情況下回調(diào)函數(shù)的順序是如何的,加法得到的結(jié)果里,回調(diào)函數(shù)的順序也是相對一樣的,不會發(fā)生變動。
另外。如果一個委托對象本身就包含了很多回調(diào)函數(shù)了,那么作為被加數(shù)或者加數(shù)放在委托加法運算里的話,那么我們這些方法也會順次加進去。比如說
handler1
已經(jīng)存儲了A
和B
兩個回調(diào)函數(shù)了,而handler2
里存儲了C
回調(diào)函數(shù)的話,那么整個加法就會得到一個委托對象,順次存儲了A
、B
和C
三個回調(diào)函數(shù)。然后在開始回調(diào)(調(diào)用handler.Invoke()
)的時候,A
、B
和C
會按照順序得到調(diào)用。
Part 2 委托的減法
這個可能就沒辦法和字符串作對比和比較了。因為字符串沒有減法運算的關(guān)系,我們無法參考來理解。委托的減法是這樣的行為:
如果兩個相同委托類型的對象作減法,假設(shè)是 handler1 - handler2
的話:
如果
handler1
里的回調(diào)函數(shù)列表的某個子序列和handler2
一樣,那么這個子序列的所有回調(diào)函數(shù)全部會被刪除;如果不滿足前一條規(guī)則(即其它情況下),
handler1
不會作任何行為,得到的結(jié)果也和handler1
這個被減數(shù)是一樣的結(jié)果。
這兩點是什么意思呢?委托實際上存儲的是一系列的回調(diào)函數(shù),它們用一個表表示出來(當然這個表里也可以只存儲一個回調(diào)函數(shù),就類似于我們上一講內(nèi)容講到的那種情況)。其中連續(xù)的若干回調(diào)函數(shù)被稱為一個子序列。如果被減數(shù) handler1
包含 handler2
連順序都一樣的回調(diào)函數(shù)序列的話,那么這一段子序列就會被減掉。比如 handler1
包含 A
到 F
六個方法,而 handler2
包含 C
、D
的話,那么 handler1 - handler2
的結(jié)果就是 A
、B
、E
、F
順次構(gòu)成的序列作為回調(diào)函數(shù)列表的委托對象,因為 C
和 D
在 handler1
里包含這個子序列。
另外,如果 handler1
還是 A
到 F
,但 handler2
的回調(diào)函數(shù)列表是 D
和 C
(注意順序反過來了,我們可能是先對 handler2
實例化了 D
作為回調(diào)函數(shù),然后才用加法運算把 C
累計到 handler2
里)的話,由于 handler1
的子序列不存在 D
和 C
這種情況,所以 handler1 - handler2
的結(jié)果和 handler1
原來的結(jié)果完全一樣。
所以,委托的加法是直接拼接起來,但委托的減法則會看回調(diào)函數(shù)列表的次序。當然,如果減法情況下,handler2
只包含一個回調(diào)函數(shù),那么肯定只要 handler1
里有這個回調(diào)函數(shù),那么百分之百都可以減掉。稍微需要注意的是次序的問題。
Part 3 多播委托的基本概念
我們把一個委托對象包含多個回調(diào)函數(shù)的時候的情況稱為多播委托(Multicast Delegate)。“多播”一詞來自于計算機網(wǎng)絡(luò)里的“單播”、“多播”、“廣播”、“組播”的“多播”,表示一種傳遞的過程,因為每一個回調(diào)函數(shù)都會得到執(zhí)行,好像第一個回調(diào)函數(shù)執(zhí)行完了就傳遞給第二個,讓第二個執(zhí)行;第二個回調(diào)函數(shù)執(zhí)行完畢了就傳遞給第三個執(zhí)行。當然,和這個概念對應(yīng)的就是單播委托(Unicast Delegate)了,表示一個委托類型的對象只包含一個回調(diào)函數(shù)的時候的情況。
Part 4 委托的基類型:MulticastDelegate
類
正是因為多播委托的名字叫做 multicast delegate 的關(guān)系,所以委托的基類型是 MulticastDelegate
類。這里稍微注意一點。雖然有的時候委托類型的對象可以只含有一個回調(diào)函數(shù)(即這個對象是個單播委托),但是這個委托對象的類型仍然是從 MulticastDelegate
類型派生出來的。這是因為這個類型本身就具有可多播委托的潛力和能力,所以它可以用在多播委托上,因此它必然是從這個類型派生的;并不是說它只有一個回調(diào)函數(shù)就不走這里派生了。希望你把這個概念搞清楚。
MulticastDelegate
其實也沒有什么要說的,因為它只是為了提供一種多播委托執(zhí)行的約束和底層的支持,所以它本身對我們實現(xiàn)代碼,調(diào)用邏輯來說都沒有多大的幫助和作用,你只需要知道它是客觀存在的就可以了。
Part 5 委托的最終基類型:Delegate
類
我們前文說過,委托類型的繼承關(guān)系很復(fù)雜,因為它的基類型就有兩個,一個是 MulticastDelegate
類,另外一個則是 Delegate
類。我們還說過,MulticastDelegate
類型還是從 Delegate
類型派生下來的。下面我們來說一下 Delegate
這個類的用法和基本內(nèi)容。
Delegate
類型也是一個抽象類。這個抽象類里提供了很多有關(guān)委托基本操作和行為的方法,比如我們要學(xué)習(xí)的有:
Delegate.Combine
靜態(tài)方法和Delegate.Remove
靜態(tài)方法Delegate.DynamicInvoke
實例方法Delegate.Equals
實例方法、Delegate.operator ==
和Delegate.operator !=
運算符
其它的還有一些別的方法,不過沒有必要講,因為用不上不說而且還比較麻煩,部分還是超綱的東西。下面我們來挨個說明。
5-1 Combine
方法:委托加法的底層
Combine
方法實際上就是委托加法運算的底層操作。如果我們要把兩個委托類型的對象加起來,我們使用加法會非常方便;不過底層的代碼是這樣的:
是的。我們使用 Delegate
自帶的靜態(tài)方法 Combine
來結(jié)合兩個同委托類型的對象。最后得到了一個相同委托類型的結(jié)果,并賦值給左側(cè)。因為 Delegate
類型的這個方法我們是無法從代碼層面知道它的具體委托類型的,所以它傳入的參數(shù)實際上是兩個 Delegate
類型的對象。同理,因為不清楚類型的原因,返回值類型也是 Delegate
類型的對象。實際上,Delegate
類型我們是無法確定具體是什么類型的,這一點和之前介紹傳入 object
的道理完全一樣,因為想把方法通用化,所以就這么干了。
正是因為返回值類型是 Delegate
這個抽象類型的緣故,我們需要強制轉(zhuǎn)換才能賦值給具體類型,因此才有了這里的 (Delegate)
強制轉(zhuǎn)換運算符。
5-2 Remove
方法:委托減法的底層
既然有加法,就有減法對應(yīng)的操作。和 Combine
方法的套路完全一樣,Remove
方法翻譯的時候也是方法傳參帶強轉(zhuǎn)。
5-3 DynamicInvoke
方法:對不知道具體類型的委托進行調(diào)用
倘若我們并不知道不清楚一個委托類型的具體類型,因為它固定從 MulticastDelegate
類型派生,而它又是 Delegate
的子類型,所以 Delegate
類型的這個方法可能對你會很有幫助。
如果方法我們只知道簽名,但委托的類型不定的時候,我們基本寸步難行。為了避免這樣的問題發(fā)生,C# 設(shè)計了一個特別神奇的方法:DynamicInvoke
。這個方法在你不知道委托類型,而只是知道委托類型的回調(diào)函數(shù)簽名的時候,就可以直接用。
@delegate
變量的回調(diào)函數(shù)方法是無參數(shù)無返回值時,就可以直接使用 DynamicInvoke
方法進行對方法的調(diào)用。如果方法帶有返回值,你還可以把整個調(diào)用表達式作為一個數(shù)值寫在賦值運算符的右側(cè)。只是說,因為你的類型不知道的關(guān)系,為了通用性的緣故,這個 DynamicInvoke
方法最終返回的類型是 object
5-4 委托的相等性比較,以及委托的結(jié)構(gòu)不一致性
如果兩個委托類型的對象要想一樣(相等),需要滿足下面的條件:
兩個委托對象的類型一樣;
兩個委托對象的回調(diào)函數(shù)列表包含的方法一樣;
兩個委托對象的回調(diào)函數(shù)的回調(diào)次序也必須一樣。
但凡其中有一個不滿足,兩個委托對象都是不相等的。下面我們來看一個例子。
如代碼所示,我們有三個委托對象 handler
、handler2
和 handler3
。請問,三個委托類型的對象是不是一樣的?
答案是,兩兩都不一樣。第一個和第二個不相等原因很簡單:handler2
最后刪除了 A
方法,所以回調(diào)函數(shù)列表就已經(jīng)不同了;而 handler3
和 handler1
的差別在于,C
和 D
加入到回調(diào)函數(shù)的次序是不一樣的。由于 handler3
是先加入了 D
后加入 C
的關(guān)系,所以和 handler1
的次序不同,因此兩個委托對象也不相等。
Equals
實例方法來獲得,也可以使用運算符 ==
和 !=
來獲得。
說完委托類型的相等性后,我們來說一下委托的結(jié)構(gòu)不一致性。這個詞語過于術(shù)語化,所以可能不太明白,你可以認為是這么一種東西:
因為委托類型是可以自定義的參數(shù)類型和返回值類型的,所以我們完全可以自定義兩個簽名完全一樣但委托類型名稱不同的委托類型。
比如上面這兩個委托類型,一個叫 Assignment
,另外一個則是叫 Handler
。雖然它們的簽名一致(無參數(shù)無返回值),但正是因為類型名本身不同,所以它們?nèi)匀皇菬o法通用的。
舉個例子。假設(shè)我有一個 Assignment
委托類型的對象,我嘗試把它賦值給 Handler
類型,可以嗎?不可以,因為類型不同。
這個稱為委托的結(jié)構(gòu)不一致性。因為委托的底層是一個類,所以兩個委托類型就對應(yīng)了兩個類。兩個不同的類就意味著無法互相轉(zhuǎn)換。你之前也學(xué)過面向?qū)ο髮Π?,你肯定知道無法隨便把兩個類型進行轉(zhuǎn)換,對吧。只有繼承關(guān)系,或者是自定義了轉(zhuǎn)換關(guān)系的運算符才允許轉(zhuǎn)換。
Part 6 為什么委托類型非要從兩個完全不一樣的基類型派生?
可能你會有這樣的問題:為什么委托類型非得用 MulticastDelegate
和 Delegate
兩個不同的基類型來作為固定的派生關(guān)系?你看別的類型,Enum
也好、Array
也好,它們雖然有的無法自定義繼承關(guān)系,但是最多也就從這一個類型進行派生。為什么委托要分兩個類型?或者換句話說,為什么不能讓 MulticastDelegate
類型的代碼內(nèi)容全部一并丟進 Delegate
類型里?這樣不就少一個類型了嗎?
下面我們來說一下原因。其實原因很簡單:做的東西和工作不一樣。面向?qū)ο笥幸粋€基本的實現(xiàn)規(guī)范,叫做單一職責原則(Single Responsibility Principle)。單一職責原則說的是:一個類型只能做一件事情,就是它這個類型本身應(yīng)該做的事情。這句話有點繞。我如果有一個 Person
類型,那么這個 Person
類型里寫的代碼就一定要跟 Person
它自己的行為有關(guān)系。比如說 Person
類型可以派生出 Teacher
子類型,但是 Teacher
可以教書但 Person
類型的對象不一定都會教書。你不能把子類型的工作放父類型里來。Delegate
只是表達委托的基本操作和行為,并不是跟多播委托綁定的概念。雖然我們經(jīng)常說,委托都是多播的,但這并不代表所有的委托都一定要用多播委托的功能;那么委托類型就得有一個委托的基類型作為服務(wù)的提供。所以,多播委托是多播委托,委托是委托。