泛型的约束不止一面
1.介绍
泛型中的约束,其实就是针对类型参数的约束,限制类型参数的选择只能在某个特定范围内。其中的体现包括:限制类型参数必须是一个结构、限制类型参数必须是某个具体类型、限制类型参数必须派生自某个基类等等。在默认情况下,定义的泛型没有任何约束,这意味着在调用泛型时,可以使用任何数据类型作为类型参数。如果定义了约束,则在应用端调用泛型时,不传入符合约束条件的类型参数,编译器将提示错误。通过这种约束实现了编译前类型检查,确保了泛型在运行时对类型参数使用的安全性。
以上说的这种限制性的作用,只能体现约束表面的用意,这种用意是比较浅显易懂。但实际上泛型的约束还有另一层的用意:“定义约束可以告知编译器,类型参数具备了哪些能力”。我们在为某个泛型类或泛型方法编码时,面向的类型参数T,其实类似是一个模糊神秘的事物,因为你根部不会知道它有什么能力(属性、方法等成员),如果你想在编写泛型时使用类型参数T的某些能力,那么你就可以通过定义约束来实现。例如,你想要类型参数T调用“比较大小”的方法从而帮助你实现排序算法,你就可以定义一个泛型的约束:“要求类型参数必须实现IComparer接口”。这样一来,你的类型参数T,就能够在你编写泛型类的代码中“.”出Compare(比较的方法)。
基于上面对类型参数定义约束的用意分析,我针对约束主要的作用总结出以下两点:
- 对外部使用形成了限制条件,从而确保泛型的类型安全;
- 对内部使用提供了更多能力,从而丰富功能的实现;
以上通过文字描述的形式介绍了泛型中类型参数的约束,为了更加形象的体会其中的含义和作用,下面我将通过代码示例的形式介绍类型参数定义约束的使用方式。
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.结语
理解泛型的约束,可能会觉得它是很语义化、片面化的东西。殊不知,其实泛型约束在实际中最有作用的是,为类型参数提供能力,让我们在编码的过程中更有针对性。所以学习不能只求表面,必须通过反复思考,才能让获取的知识更加立体。
对于泛型约束的使用方式,除了本文示例中要求实现一个特定接口的方式,另外还有很多使用方式。我们不可能将每一个使用细节了然于心,但是必须搞清楚事物的本质,以致于知道为什么有它的存在、在什么样的情况下使用它。当不同的应用场景发生时,我们在结合当下应用场景的实际情况,通过查阅文档来制定具体的方针。