3

泛型的约束不止一面 - 姜承轩

 2 years ago
source link: https://www.cnblogs.com/green-jcx/p/16700457.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

泛型的约束不止一面

泛型中的约束,其实就是针对类型参数的约束,限制类型参数的选择只能在某个特定范围内。其中的体现包括:限制类型参数必须是一个结构、限制类型参数必须是某个具体类型、限制类型参数必须派生自某个基类等等。在默认情况下,定义的泛型没有任何约束,这意味着在调用泛型时,可以使用任何数据类型作为类型参数。如果定义了约束,则在应用端调用泛型时,不传入符合约束条件的类型参数,编译器将提示错误。通过这种约束实现了编译前类型检查,确保了泛型在运行时对类型参数使用的安全性。

以上说的这种限制性的作用,只能体现约束表面的用意,这种用意是比较浅显易懂。但实际上泛型的约束还有另一层的用意:“定义约束可以告知编译器,类型参数具备了哪些能力”。我们在为某个泛型类或泛型方法编码时,面向的类型参数T,其实类似是一个模糊神秘的事物,因为你根部不会知道它有什么能力(属性、方法等成员),如果你想在编写泛型时使用类型参数T的某些能力,那么你就可以通过定义约束来实现。例如,你想要类型参数T调用“比较大小”的方法从而帮助你实现排序算法,你就可以定义一个泛型的约束:“要求类型参数必须实现IComparer接口”。这样一来,你的类型参数T,就能够在你编写泛型类的代码中“.”出Compare(比较的方法)。

基于上面对类型参数定义约束的用意分析,我针对约束主要的作用总结出以下两点:

  1. 对外部使用形成了限制条件,从而确保泛型的类型安全;
  2. 对内部使用提供了更多能力,从而丰富功能的实现;

以上通过文字描述的形式介绍了泛型中类型参数的约束,为了更加形象的体会其中的含义和作用,下面我将通过代码示例的形式介绍类型参数定义约束的使用方式。


假设我们在一个开发游戏的背景下,游戏比较简单,其中目前有两个职业:剑士和狙击手,并且后期随着游戏的普及会增加更多的职业。由于是战斗类型的游戏,所有每个职业都会使用特定的武器进行攻击,从而实现战斗的体验。对于该游戏职业设计相关的类图如下:

 

722260-20220916162909065-1018744037.png

由于这只是一个为了讲解泛型约束的一个示例,所以并没有采用复杂的设计。由于剑士和狙击手两个职业都有相同的攻击行为,故而将攻击定义为了一个接口,具体的攻击内容将交由这两个职业类去实现。根据以上的类图的设计,相应的代码如下:

 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     }

假设我们的游戏示例是一款战斗类型的游戏,那么其中所有的职业都需要进行战斗。对于这个共同的行为,正好可以借鉴泛型的使用思想:即不同类型存在相同处理逻辑,那么可以使用泛型作为一个代码模板,从而实现不同类型的通用化处理。我们计划将战斗的行为定义成一个泛型类,由这个泛型类统一实现各个职业的战斗。然而在编写战斗泛型类的时候,由于战斗必须要使用职业的攻击方法,但是我们在内部调用类型参数T并不能获取到相应的方法,编译器视乎将类型参数T看成了一个object类型。

722260-20220916163043041-1175853592.png

怎么办?究竟如何能够在战斗泛型类中调用游戏角色的攻击方法呢?这个时候就轮到本文的主题“泛型的约束”闪亮登场了,接下来我们将针对战斗泛型类定义一个约束,在泛型类中使用类型参数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就可以根据约束的类型获取相应的能力。这一点也正好可以印证了本文开头总结泛型约束的作用之一:对内部使用提供了更多能力,从而丰富功能的实现”。示例的代码已经基本编写完成,接下来我们就可以在应用端,使用战斗泛型类针对不同的角色实施战斗行为了。

 

722260-20220916163133963-1454665301.png

假设你的小伙伴正在另一头在编写游戏中关于NPC部分的代码,他得知你编写了可以实现各种职业进行战斗的泛型类,于是乎他悄悄的使用一个NPC的对象来使用你的战斗泛型类。但是NPC在实际的需求中并没有实现攻击接口。NPC类的代码结构如下:

722260-20220916163208163-1655920199.png

我们在假定,泛型的约束不能够对外部传入的类型参数(NPC类)起到限制作用。那么这个NPC的“战斗”情况可想而知,NPC是没有主动攻击的方法的,他盲目的使用战斗泛型类,只会无情的面临“死亡”。还好,我们定义的类型参数约束对此进行了把关,我们约束的规则是:要求类型参数必须实现攻击接口。而NPC并没有实现攻击接口,所以对于NPC使用战斗泛型类时编译器会提示错误。

 

722260-20220916163232459-562538451.png

通过NPC滥用泛型类的这个示例,就可以从分的体现出本文开头总结泛型约束的作用之一:“对外部使用形成了限制条件,从而确保泛型的类型安全”。


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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK