泛型的約束不止一面

2022-09-16 18:02:08

1.介紹

泛型中的約束,其實就是針對型別引數的約束,限制型別引數的選擇只能在某個特定範圍內。其中的體現包括:限制型別引數必須是一個結構、限制型別引數必須是某個具體型別、限制型別引數必須派生自某個基礎類別等等。在預設情況下,定義的泛型沒有任何約束,這意味著在呼叫泛型時,可以使用任何資料型別作為型別引數。如果定義了約束,則在應用端呼叫泛型時,不傳入符合約束條件的型別引數,編譯器將提示錯誤。通過這種約束實現了編譯前型別檢查,確保了泛型在執行時對型別引數使用的安全性。

以上說的這種限制性的作用,只能體現約束表面的用意,這種用意是比較淺顯易懂。但實際上泛型的約束還有另一層的用意:「定義約束可以告知編譯器,型別引數具備了哪些能力」。我們在為某個泛型類或泛型方法編碼時,面向的型別引數T,其實類似是一個模糊神祕的事物,因為你根部不會知道它有什麼能力(屬性、方法等成員),如果你想在編寫泛型時使用型別引數T的某些能力,那麼你就可以通過定義約束來實現。例如,你想要型別引數T呼叫「比較大小」的方法從而幫助你實現排序演演算法,你就可以定義一個泛型的約束:「要求型別引數必須實現IComparer介面。這樣一來,你的型別引數T,就能夠在你編寫泛型類的程式碼中「.」出Compare(比較的方法)。

基於上面對型別引數定義約束的用意分析,我針對約束主要的作用總結出以下兩點:

  1. 對外部使用形成了限制條件,從而確保泛型的型別安全;
  2. 對內部使用提供了更多能力,從而豐富功能的實現;

以上通過文字描述的形式介紹了泛型中型別引數的約束,為了更加形象的體會其中的含義和作用,下面我將通過程式碼範例的形式介紹型別引數定義約束的使用方式。


2.範例

假設我們在一個開發遊戲的背景下,遊戲比較簡單,其中目前有兩個職業:劍士和狙擊手,並且後期隨著遊戲的普及會增加更多的職業。由於是戰鬥型別的遊戲,所有每個職業都會使用特定的武器進行攻擊,從而實現戰鬥的體驗。對於該遊戲職業設計相關的類圖如下:

 

由於這只是一個為了講解泛型約束的一個範例,所以並沒有採用複雜的設計。由於劍士和狙擊手兩個職業都有相同的攻擊行為,故而將攻擊定義為了一個介面,具體的攻擊內容將交由這兩個職業類去實現。根據以上的類圖的設計,相應的程式碼如下:

 1     //攻擊介面
 2     interface IAttack
 3     {
 4         void MeleeAttacks();  //近戰攻擊
 5     }
 6 
 7     //劍士
 8     class Swordman: IAttack
 9     {
10         public Swordman() => Sword = "倚天劍";
11 
12         public string Sword { get; set; }
13         public void MeleeAttacks()
14         {
15             Console.WriteLine("使用{0}進行刺擊。", Sword);
16         }
17     }
18 
19     
20     //狙擊手
21     class Sniper : IAttack
22     {
23         public Sniper() => Gun = "98k狙擊步槍";
24         public string Gun { get; set; } //
25  
26         public void MeleeAttacks()
27         {
28             Console.WriteLine("使用{0}進行射擊。", Gun);
29         }
30     }

3.能力

假設我們的遊戲範例是一款戰鬥型別的遊戲,那麼其中所有的職業都需要進行戰鬥。對於這個共同的行為,正好可以借鑑泛型的使用思想:即不同型別存在相同處理邏輯,那麼可以使用泛型作為一個程式碼模板,從而實現不同型別的通用化處理。我們計劃將戰鬥的行為定義成一個泛型類,由這個泛型類統一實現各個職業的戰鬥。然而在編寫戰鬥泛型類的時候,由於戰鬥必須要使用職業的攻擊方法,但是我們在內部呼叫型別引數T並不能獲取到相應的方法,編譯器視乎將型別引數T看成了一個object型別。

怎麼辦?究竟如何能夠在戰鬥泛型類中呼叫遊戲角色的攻擊方法呢?這個時候就輪到本文的主題「泛型的約束」閃亮登場了,接下來我們將針對戰鬥泛型類定義一個約束,在泛型類中使用型別引數T呼叫出攻擊的方法:

 1     /// <summary>
 2     /// 各個職業的戰鬥
 3     /// </summary>
 4     class Combat<T> where T :IAttack
 5     {
 6         public Combat(T combatant)
 7         {
 8             _combatant = combatant;
 9         }
10         private T _combatant;//參戰者
11 
12         public void Action()
13         {
14             Console.WriteLine("戰鬥開始");
15             _combatant.MeleeAttacks();
16             Console.WriteLine("戰鬥結束");
17         }
18 
19     }

果不其然,成功的在戰鬥泛型類中呼叫了角色的攻擊方法,這是因為設定了約束,型別引數T就可以根據約束的型別獲取相應的能力。這一點也正好可以印證了本文開頭總結泛型約束的作用之一:對內部使用提供了更多能力,從而豐富功能的實現」。範例的程式碼已經基本編寫完成,接下來我們就可以在應用端,使用戰鬥泛型類針對不同的角色實施戰鬥行為了。

 


4.安全

假設你的小夥伴正在另一頭在編寫遊戲中關於NPC部分的程式碼,他得知你編寫了可以實現各種職業進行戰鬥的泛型類,於是乎他悄悄的使用一個NPC的物件來使用你的戰鬥泛型類。但是NPC在實際的需求中並沒有實現攻擊介面。NPC類的程式碼結構如下:

我們在假定,泛型的約束不能夠對外部傳入的型別引數(NPC類)起到限制作用。那麼這個NPC的「戰鬥」情況可想而知,NPC是沒有主動攻擊的方法的,他盲目的使用戰鬥泛型類,只會無情的面臨「死亡」。還好,我們定義的型別引數約束對此進行了把關,我們約束的規則是:要求型別引數必須實現攻擊介面。而NPC並沒有實現攻擊介面,所以對於NPC使用戰鬥泛型類時編譯器會提示錯誤。

 

通過NPC濫用泛型類的這個範例,就可以從分的體現出本文開頭總結泛型約束的作用之一:「對外部使用形成了限制條件,從而確保泛型的型別安全」。


5.結語

理解泛型的約束,可能會覺得它是很語意化、片面化的東西。殊不知,其實泛型約束在實際中最有作用的是,為型別引數提供能力,讓我們在編碼的過程中更有針對性。所以學習不能只求表面,必須通過反覆思考,才能讓獲取的知識更加立體。

對於泛型約束的使用方式,除了本文範例中要求實現一個特定介面的方式,另外還有很多使用方式。我們不可能將每一個使用細節瞭然於心,但是必須搞清楚事物的本質,以致於知道為什麼有它的存在、在什麼樣的情況下使用它。當不同的應用場景發生時,我們在結合當下應用場景的實際情況,通過查閱檔案來制定具體的方針。