第 95 講:C# 3 之分部方法
下面我們要介紹兩個(gè)方法的語法拓展特性:分部方法和擴(kuò)展方法。先來看簡單的:分部方法(Partial Method)。
Part 1 partial
用在方法上
考慮一種情況。假設(shè)我對(duì)這個(gè)類型實(shí)現(xiàn)了眾多的情況,它們完成的任務(wù)各不相同。
比如我有一個(gè) Work
類型,里面包含 Do
方法表示完成一個(gè)任務(wù)。這個(gè)任務(wù)有眾多的步驟需要我們挨個(gè)完成。我們假設(shè)它們分別表示為不返回任何數(shù)值的 Step1
、Step2
、Step3
方法(當(dāng)然也可以有 Step4
等等,這里就假設(shè)三個(gè))。
問題是,我把這個(gè)實(shí)現(xiàn)過程的步驟定義好了,卻在發(fā)現(xiàn)計(jì)劃只完成了一半。另一半則是實(shí)現(xiàn) Step1
、Step2
、Step3
方法。但問題是,有些時(shí)候我們不總是非得需要將所有的步驟全給實(shí)現(xiàn)了,比如調(diào)試的時(shí)候。假如我只是想試著調(diào)試運(yùn)行一下前面的步驟是否成功執(zhí)行的話,那么此時(shí)可能 Step1
、Step2
由我們實(shí)現(xiàn)完成,而 Step3
是沒邏輯的。
那么,代碼可以成這樣:
此時(shí)我們不得不屏蔽 Step3
,或是試著實(shí)現(xiàn)一個(gè)空的 Step3
方法。
不過有些時(shí)候,這樣的實(shí)現(xiàn)并不是完美的。因?yàn)樗粔蜢`活:我們必須屏蔽一些我們暫時(shí)不需要的代碼,或者是強(qiáng)制性地實(shí)現(xiàn)一些地方的代碼,只不過給大括號(hào)里寫成空的而已。于是,分部方法就誕生了。
我們試著替換 private
關(guān)鍵字,改成 partial
關(guān)鍵字,然后把實(shí)現(xiàn)代碼單獨(dú)放在一個(gè)文件里,而聲明則放在當(dāng)前文件里。舉個(gè)例子,假設(shè)我主要實(shí)現(xiàn)邏輯(包含 Do
方法的這個(gè)文件)放下面的這些代碼:
partial
關(guān)鍵字;而第 12 到第 14 行代碼的寫法則以分號(hào)結(jié)尾。然后我們試著創(chuàng)建一個(gè)新的文件叫 Work.Impl.cs
,表示實(shí)現(xiàn)部分。此時(shí) Step3
Step1
方法和 Step2
方法的邏輯,而 Step3
我們不去關(guān)心它有沒有實(shí)現(xiàn)。注意關(guān)鍵字使用:因?yàn)轭愋头譃閮蓚€(gè)文件存儲(chǔ),一個(gè)是定義,一個(gè)是實(shí)現(xiàn),所以文件都得帶上 partial
修飾符暗示兩個(gè)類型是同一個(gè)類型,但放在不同的文件里。然后在實(shí)現(xiàn)的方法上,將 private
改為 partial
。不過一個(gè)文件表示的是定義方法的簽名,而帶 Impl
后綴的文件,則是去實(shí)現(xiàn)我們需要實(shí)現(xiàn)的部分。
這個(gè)語法是 C# 3 誕生的新語法:分部方法。分部方法允許我們在同一個(gè)類型里將方法分為兩個(gè)文件存儲(chǔ):定義和實(shí)現(xiàn)。分離定義和實(shí)現(xiàn),是分部方法的目的和效果。
Part 2 為什么設(shè)計(jì)這么古怪?
可能你會(huì)覺得,這種設(shè)計(jì)的目的和意義。好吧實(shí)際上這么設(shè)計(jì)代碼是有用途的。我們最常用的一個(gè)點(diǎn)是,分部方法可以不實(shí)現(xiàn),即提前用簽名來調(diào)用方法。如果方法沒有實(shí)現(xiàn),編譯器會(huì)將其自動(dòng)刪除掉。
考慮前面的代碼。我們?nèi)绻@么去設(shè)計(jì)一個(gè)類的話,我們可以定義一大堆的 partial void 方法(參數(shù))
的簽名,但不去實(shí)現(xiàn)它。我們可以只挑選其中一些去實(shí)現(xiàn),它也不影響代碼的運(yùn)行,編譯仍舊是通過的。就像是上面給的這個(gè)例子里,partial void 方法(參數(shù))
格式語法的地方有三處,分別是 Step1
、Step2
和 Step3
這三個(gè)方法的簽名定義。它們定義后直接以分號(hào)就結(jié)尾了。只要我們實(shí)現(xiàn)了它,那么在 Do
方法里就會(huì)執(zhí)行;相反如果不實(shí)現(xiàn),那么你不更改 Do
里的代碼也是 OK 的,但是因?yàn)樗鼪]有實(shí)現(xiàn),因此會(huì)被編譯器自動(dòng)刪除。畢竟反正也沒給出實(shí)現(xiàn),保留也沒有意義。
Part 3 分部方法的限制
這種設(shè)計(jì)是有道理的,但正是因?yàn)橛羞@種設(shè)計(jì)規(guī)定,因此也確實(shí)擁有很多限制。下面我們來說一下它們。
3-1 分部方法默認(rèn)為 private
級(jí)別;不允許修飾訪問修飾符
可以看出,這樣的代碼是類型內(nèi)級(jí)別訪問。因?yàn)樗挥脕韮?nèi)部討論和操作,如果暴露到任何別的地方的話,都會(huì)導(dǎo)致濫用和錯(cuò)誤使用,因此這種限制是有必要的。
C# 3 的分部方法語法規(guī)定,我們給的分部方法默認(rèn)就是私有的。而正是因?yàn)樗悄J(rèn)的也是不可改變的,因此分部方法甚至不允許顯式地給出任何訪問修飾符的關(guān)鍵字,即使是 private
也不行。
3-2 分部方法必須不返回任何數(shù)值
C# 3 也不允許分部方法返回任何數(shù)值。這是因?yàn)?,返回值可能?huì)改變方法的執(zhí)行行為。比如說下面的代碼:
F
是分部方法的話,那么如果方法沒有給予實(shí)現(xiàn)部分,那么這個(gè) result
就不可能存在。因?yàn)?F
會(huì)被編譯器自動(dòng)刪除,但 result
的牽連會(huì)使得編譯器處理起來困難不少——我后面所有使用 result
的地方也都得刪除,說不定任何用到 result
的地方,也會(huì)被改變邏輯和執(zhí)行意義。因此,這樣的問題是顯著的,因此,C# 3 分部方法不允許返回值。正是因?yàn)檫@個(gè)原因,partial
關(guān)鍵字后必須跟 void
3-3 分部方法不能帶有 out
參數(shù)
既然返回值都不允許了,那么輸出參數(shù)也肯定不能被允許。因?yàn)?out
修飾的參數(shù)是從參數(shù)位置去返回的類型,它解決的是方法只返回一個(gè)數(shù)的問題。
out
,而 ref
是可以修飾的。有人可能會(huì)問,為啥 ref
可以?ref
不是也是跟 out
差不多,只是 ref
雙向,而 out
注意分部方法的實(shí)現(xiàn),參數(shù)是有 ref
修飾符的。
下面我們調(diào)用它。
F
是的,這樣的代碼因?yàn)?x
并未和方法關(guān)聯(lián)起來,傳入引用也只是是理論上可以在 F
方法里改變 x
的數(shù)值,但實(shí)際上,如果我不調(diào)用它的話,編譯器也知道,F(ref x)
調(diào)用被刪除不影響代碼的編譯。
這里插一句你可能沒有注意過的細(xì)節(jié)。不論是不是分部方法,所有方法里只要帶有
ref
參數(shù)的方法,那么編譯器都會(huì)要求你調(diào)用傳參之前必須有初始值。比如說這里的int x = 20;
,如果在調(diào)用F(ref x)
之前沒有給x
賦初始值,編譯器會(huì)直接報(bào)錯(cuò)。因?yàn)檫@樣的調(diào)用過程就有可能因?yàn)閷?shí)現(xiàn)F
方法里改動(dòng)了它的數(shù)值。因?yàn)榉椒w里是無法知道x
是否有初始化。如果x
沒初始化就在使用,這必將導(dǎo)致代碼執(zhí)行起來的不安全性。因此為了嚴(yán)謹(jǐn),編譯器會(huì)要求你必須對(duì)ref
參數(shù)傳參的這個(gè)變量先具有初始值之后才可以使用起來。
3-4 分部方法修飾符只允許 static
和 unsafe
;別的都不行
C# 誕生了各種各樣的類型后,也擁有了完成的繼承體系。在 C# 里,關(guān)于繼承的關(guān)鍵字有這些:
sealed
關(guān)鍵字;abstract
關(guān)鍵字;virtual
關(guān)鍵字;override
關(guān)鍵字;new
關(guān)鍵字。
不過,這些關(guān)鍵字全部都不能用于分部方法。原因很簡單:因?yàn)槔^承機(jī)制無法防止你的實(shí)現(xiàn)的派生和重寫,導(dǎo)致代碼的不安全。舉個(gè)例子:
這種就是典型的錯(cuò)誤使用。因?yàn)?F
本身就是抽象的了,它本身就不應(yīng)該有實(shí)現(xiàn)部分。但是,你對(duì)這種本身就抽象的東西修飾 partial
是什么意思?分部方法意味著你可以按需在別的文件里給出 F
的實(shí)現(xiàn),但 abstract
修飾符又防止你在這個(gè)類型里去實(shí)現(xiàn)它,這不是自相矛盾的嗎?
在派生和繼承機(jī)制下,上述五個(gè)修飾符全部都不行,別的四個(gè)你就自己分析思考一下為什么吧。
哦對(duì),extern
關(guān)鍵字也不行,雖然這個(gè)修飾符用于導(dǎo)入 dll 文件里帶有的庫函數(shù)。但是,這樣的方法本身就不可能有實(shí)現(xiàn),因?yàn)閷?shí)現(xiàn)并不在 C# 語言里實(shí)現(xiàn),而是在 dll 文件里,因此它本身就沒有實(shí)現(xiàn)機(jī)制,那么 partial
就沒有意義了。
所以,上述的修飾符一個(gè)都不行。除了 static
。
unsafe
這樣就行。用法也很簡單。
比如這樣。
3-5 分部方法必須用于分部類型里
這個(gè)是顯而易見的。你要用分部類,那么你有可能會(huì)對(duì)這個(gè)方法給予實(shí)現(xiàn)。那么既然有可能提供實(shí)現(xiàn),那么自然就需要一個(gè)環(huán)境提供這樣的行為可以使用。那么分部方法必須放在分部類型里就先得非常正常了。
Part 4 泛型分部方法
分部方法牛逼就牛逼在它可以使用泛型??紤]內(nèi)部排序方法:
這便是一個(gè)實(shí)現(xiàn)。假設(shè)我使用接口來完成這個(gè)任務(wù)的話,那么我們需要設(shè)置泛型約束。
可以從這個(gè)例子里看出,我們具有的泛型參數(shù)必須是值類型,而且還得是實(shí)現(xiàn)了大小比較行為的類型,這樣我們才能確保操作能夠大小比較成功,并且交換成功。
顯然,調(diào)用方就不多說了。這里關(guān)注一下代碼的寫法。這里要注意兩個(gè)地方。
4-1 泛型參數(shù)是可以換名字的
可以看到,泛型參數(shù)在定義里用的是 TComparable
,而實(shí)現(xiàn)里卻用的是 T
。這是允許的。原因是編譯器對(duì)泛型的分部方法如何看待的。編譯器對(duì)泛型參數(shù)的名字并不關(guān)心,它只關(guān)心泛型參數(shù)的名字是否有重復(fù),個(gè)數(shù)有幾個(gè),都對(duì)應(yīng)哪里。舉個(gè)例子,A<T>
和 A<TypeParameter>
是同一個(gè)類型,雖然泛型參數(shù)名字不同,但大家都知道,泛型參數(shù)在底層的實(shí)現(xiàn)機(jī)制,是看個(gè)數(shù)不同來區(qū)分類型的。因此,對(duì)于上面的分部方法來說,泛型參數(shù)的名字換沒換其實(shí)并不重要。
4-2 泛型參數(shù)的約束是必須重復(fù)的
雖然泛型約束不是簽名的一部分,但只在簽名里書寫,或者只在實(shí)現(xiàn)里書寫泛型約束,都是不夠明顯的。因此,你必須重復(fù)寫泛型約束,便于編譯器分析的同時(shí)還能更輕松地掌握?qǐng)?zhí)行過程。
不過,這一點(diǎn)在官方要求的文檔里并不是這么寫的。C# 的里說,“泛型約束用于分部方法的定義部分,但實(shí)現(xiàn)部分是可以不寫的”。

但事實(shí)是,你這么寫代碼,會(huì)收到編譯器錯(cuò)誤,告訴你“定義和實(shí)現(xiàn)上,泛型約束有所不同”。

是的,就是這么離譜。
考慮如下代碼:
F
方法有實(shí)現(xiàn),但 G
方法沒有。有些人會(huì)為了去鉆空子去試試看,有沒有辦法引用到 G
請(qǐng)問,這樣的代碼對(duì)不對(duì)?答案肯定不對(duì),而且是第 2 行必然報(bào)錯(cuò)。原因很簡單,因?yàn)?G
方法沒有實(shí)現(xiàn),因此不論如何你都是沒有辦法去調(diào)用它的。哪怕是委托意味著“我們稍后調(diào)用”,但也由于沒有實(shí)現(xiàn),因此這樣的機(jī)制是不成立的。
因此,分部方法也是做了一個(gè)相當(dāng)嚴(yán)謹(jǐn)?shù)拇胧┤シ乐挂匀魏涡问秸{(diào)用。