泛型的约束不止一面

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.结语

理解泛型的约束,可能会觉得它是很语义化、片面化的东西。殊不知,其实泛型约束在实际中最有作用的是,为类型参数提供能力,让我们在编码的过程中更有针对性。所以学习不能只求表面,必须通过反复思考,才能让获取的知识更加立体。

对于泛型约束的使用方式,除了本文示例中要求实现一个特定接口的方式,另外还有很多使用方式。我们不可能将每一个使用细节了然于心,但是必须搞清楚事物的本质,以致于知道为什么有它的存在、在什么样的情况下使用它。当不同的应用场景发生时,我们在结合当下应用场景的实际情况,通过查阅文档来制定具体的方针。

posted @ 2022-09-16 16:46  姜承轩  阅读(710)  评论(0编辑  收藏  举报