让泛型的思维扎根在脑海——深刻理解泛型

1.前言

往往一些刚接触C#编程的初学者,对于泛型的认识就是直接跳到对泛型集合的使用上,虽然微软为我们提供了很多内置的泛型类型,但是如果我们只是片面的了解调用方式,这会导致我们对泛型盲目的使用。至于为什么要使用泛型,什么情况下定义属于自己的泛型,定义泛型又能为程序带来哪些好处。要理清这些问题,我们就必须深刻理解泛型的本质,形成泛型编程的思维方式。

接下来我将基于一个基础示例,然后通过需求不断的演化示例,从而让泛型在关键时刻脱颖而出,以便让我们能够深刻体会泛型的作用。假设.NET没有为我们提供用于存储数据的集合,而我们需要一个能够用于存储string元素的集合,基于这个情况我们自定义了一个用于存储字符串的集合类:

class ArraryStr{public ArraryStr(){_items = new string[100]; //初始化存储元素的容量,只是为了演示故将容量定义为固定值}private string[] _items; //存储元素的数组private int _count;   //元素总数public int Count{get { return _count; }}public void Add(string item) //新增元素{_items[_count] = item;_count++;}public string this[int index] //索引{get { return _items[index]; }set { _items[index] = value;  }}} // END ArraryStr

为了验证自定义string集合的可行性,我们对其进行了如下的应用:

1             ArraryStr arraryStr = new ArraryStr();
2             arraryStr.Add("张三");
3             Console.WriteLine(arraryStr[0]);

2.重复

目前对于创建string类型的集合已经大功告成,而此刻我们又接到了一个新的需求,即我们需要一个集合存储int类型的元素。基于自定义string集合的经验来看,我们可以发现,string集合类型和我们即将要创建的int集合类型的结构和内容几乎是一样的。这就意味着我们可以使用江湖盛行的“复制大法”,将之前的代码复制一遍,然后轻微修改下即可。下面是两个集合类型代码的对比图。

39b22390ccbadefd3d2d29e0be2b633a.png

在早年有款热门的游戏叫做“大家来找茬”,该游戏主要玩法就是在两个大致相同的图片中,查找两者之间的细微差异之处。我们使用的“复制大法”,促使我们编写的代码形成了可以用于这个游戏游玩的场景。“对于上面的两个代码截图,你能找出图中不同的地方吗?”

对于软件开发者而言,面对的最主要的敌人就是“变化”,假设后面还会出现N个类型的元素需要我们定义集合来存储,那我们是不是要将相同的代码无穷尽的复制下去?DRY(Don't Repeat Yourself,不要重复自己,请记住这是作为一名软件开发者编码的原则,“复制大法”很明显的违背了这个原则。


3.安全和性能

通过“复制,粘贴”的手段可以很明显的感受到我们在做重复的事情,在重复中我们可以发现:集合存储的类型在增加,但是集合的结构和添加元素的方法都是相同的逻辑。简单来说就是,不同类型的处理,其处理逻辑都是类似的。基于这个特点,为了满足自定义集合能够应对所有类型的存储,我们必须使用一个通用类型来作为代表,此时此刻我们脑海中就能浮现出一句话:object是一切类型的基类。这就意味着我们添加的所有类型,都可以隐式的转换为object类型,从而使得自定义集合可以添加任何类型的元素。让我们来运用这个object类型来试试:

class ArraryList{public ArraryList() { _items = new object[100]; }private object[] _items;private int _count;public int Count{get { return _count; }}public void Add(object item){_items[_count] = item;_count++;}public object this[int index]{get { return _items[index]; }set { _items[index] = value; }}} // END ArraryStrinternal class Program{static void Main(string[] args){ArraryList arraryList = new ArraryList();arraryList.Add("张三");arraryList.Add(18);string name = (string)arraryList[0];int age = (int)arraryList[1];} // END Main()}

在上面的代码中,我们结合了object是一切类型基类的特点,对集合类型进行改造,并成功的使用该方式的集合添加了不同类型的元素。虽然在使用的角度来看已经完美无缺(可以添加任何类型),但是获取集合元素进行赋值的时候,还使用了类型强制转换的手段。这是因为这种方式存在很严重的问题,主要包括以下两个方面:

  1. 类型安全方面,如果集合的第一个元素是sting类型,但是你客观认为是int类型,于是你在获取时进行了int类型的强制转换,这个时候代码不会提示错误且可以正常编译,那么这就意味着程序在运行时会产生一个你无法预料的类型无效转换的异常。

  2. 性能方面,值类型元素添加到集合时,必然会存在装箱操作;而在获取元素并赋值给一个值类型变量时,又会发生相应的拆箱操作。这种拆箱和装箱的操作,在操作大量元素时会大幅度的损失程序的性能。

到目前位置,我们还是没有能创建一个能够存储任何类型的集合,但是我们可以对于上述的示例演变的过程进行一个总结:对于不同类型有相同处理逻辑的情况,如果一味的复制会导致我们出现重复代码,如果使用object来作为解决重复的方案,会存在类型安全和性能的问题。至于如何让彻底解决这些问题,这就要说到了本文讲解的主题——泛型。


4.代码模板

C#中有两种不同的机制来编写跨类型(一个类型代替多个类型)可复用的代码:继承和泛型。继承的复用性来自于基类,而泛型的复用性是通过带有“占位符”的代码模板类型实现的。继承实现复用是站在面向对象的角度思考的,而泛型的复用是站在实现特定功能上思考的。相比于继承,泛型不用遵循里氏替换原则,并且能够提高类型的安全性,减少类型转换带来的拆箱和装箱。

怎么样理解泛型?泛型本质上相当于一种“代码模板”,可以用一套代码,为不同类型的同一逻辑使用统一的方式实现。其中“模板”一词的概念需要进行深刻的体会。例如,公司在招聘时会与用人方签订劳动合同,而这个劳动合同的主要内容对于所有人来说几乎都是一样的,只是在极个别的地方有所差异,如薪资、姓名等。所以公司不会为某个人(张三或李四)去特意的制定合同,而是会统一制定一份劳动合同作为模板,将其中针对个人存在差异的部分通过“下划线”进行占位预留,“下划线”的值将在签订合同时由具体的聘用者根据自身情况填写。

5350e87f8446c3228d1c9445c3622406.png

对于这种模板方式的使用,公司在制定合同时则不用考虑签订合同的人具体是谁,因为劳动合同(模板)和使用者是分开的,所以公司只用专注于合同的主要内容即可。而我们在实际的编程运用中,使用泛型的目的,其实和公司制定通用的劳动合同模板是一个道理。假设你的公司需要雇佣100名员工时,你不希望为每一个人都制定一个专属的合同吧?假设你的代码中,如果遇到10个类型,它们的操作处理逻辑都一样时,你不希望为这个10个类型写10个处理方式吧?

通过上面的介绍和例子,接下来我们将泛型运用到我们的示例中来,代码如下:

1     class ArraryList<T>2     {3         public ArraryList() { _items = new T[100]; }4 5         private T[] _items;6         private int _count;7         public int Count8         {9             get { return _count; }
10         }
11 
12         public void Add(T item)
13         {
14             _items[_count] = item;
15             _count++;
16         }
17 
18         public T this[int index]
19         {
20             get { return _items[index]; }
21             set { _items[index] = value; }
22         }
23     } // END ArraryStr
24     internal class Program
25     {
26         static void Main(string[] args)
27         {
28             ArraryList<string> arraryStr = new ArraryList<string>();
29             arraryStr.Add("张三");
30             Console.WriteLine(arraryStr[0]);
31 
32             ArraryList<int> arraryInt = new ArraryList<int>();
33             arraryInt.Add(18);
34             Console.WriteLine(arraryInt[0]);
35 
36         } // END Main()
37 
38     }

5.类型参数

在上面的代码中,我们将集合类型定义为了泛型类,该类型中出现的T属于泛型中的类型参数(Type Parameter)。泛型为了达到通用处理的目的,所以不能将某个具体类型作为处理的目标类型,故而将要处理的类型用“T”作为一个类型占位符。

“T”并不是真正的数据类型,它更像是泛型使用的类型蓝图,所以在使用时,泛型类型的消费者必须将一个具体类型作为“类型参数”传递到尖括号内,以此构造一个有明确处理类型的泛型实例。所以我们在外部使用泛型时不能以:“ArraryList<T>list =new ArraryList<T>()”、“T t=new T()”这种方式去实例化泛型类型。另外,“T”本身仅仅是类型参数的名称,它只是代表了类型参数的标识而已,这意味着我们可以使用其他字符来为类型参数命名。


6.替换

通过类型参数的使用我们可以得知,泛型类型代码在静态阶段没有明确的类型,那么在程序运行的时候,它又是如何和使用时指定的“类型参数”进行对接的呢?为了搞清楚这个问题,下面我们来了解下泛型运行时的本质。

我们编写的C#程序在编译后生成的代码,并不是计算机可以直接执行的代码,而是会生成CIL(通用中间语言)代码并包含在程序集中,如果想要生成计算机可执行的代码,则还需要JIT(即时编译器)对CIL代码进行二次编译。然而泛型类型确认其具体类型的时机,就在JIT进行二次编译时,JIT编译的代码如果包含了泛型的内容,那么它会根据泛型类型的消费者指定的类型参数,将CIL中泛型代码中的占位符T替换为一个具体的类型,从而明确当前执行的泛型代码是针对哪个类型来使用的,其中替换的过程是由CLR在运行时进行主导,JIT来实际操作完成的。这个在运行时确认了类型的泛型又被称之为“封闭类型”,反之在运行时确认之前的泛型称为“开放类型”。

a38c3915518c1d49479660bdae2ffa5e.png

泛型使用占位符在运行时替换具体类型的机制,其实和本文中例举劳动合同模板使用“下划线”的方式有同样的思想。在指定劳动合同模板时,对于聘用者的姓名并不能写一个具体的名字,因为模板的目的是为了通用化,所以对于名字采用了“下划线”的方式。当公司与某个具体的人签订合同的时候,劳动合同模板中的下划线将由聘用者根据自身情况填写。回到泛型中其使用思想也是如此,我们使用泛型的目的是为了让多个类型的处理通用化,所以在定义泛型代码的时候并不能指定一个具体类型,故使用类型参数T进行代替,这个类型参数T就相当于劳动合同模板中的“下划线”,当泛型在实际运行的时候,JIT会根据泛型消费者指定的具体类型与占位符T进行替换。


7.总结

本文并不是专门适用于介绍泛型的使用细节的文章,而是通过一个实例根据需求不断演化的过程,对泛型一步步深入,从而更加深刻的理解泛型的使用初衷,相比了解泛型“只言片语”而言,形成泛型的编程概念和思维显得尤为重要。在泛型的机制中,我们可以将不同类型存在相同处理逻辑的情况,形成一个通用的方案,从而不在为特定的类型进行编码,用一套通用的代码模板会服务于更多的类型,并且在使用上能保证类型安全和提供良好的性能。

原文地址:https://www.cnblogs.com/green-jcx/p/16671687.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282794.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android 系统ui修改器,分享两个效果 - Android 系统 UI 管理

SystemUIManage.gifDimming the System Bars (沉浸模式)知乎 和 Medium 中都使用到了这个效果&#xff0c;作为沉浸式阅读模式。// This example uses decor view, but you can use any visible view.View decorView getWindow().getDecorView();int uiOptions View.SYSTEM_U…

打游戏要存进度-备忘录模式

打游戏要存进度-备忘录模式 学习自 《大话设计模式》 备忘录模式漫谈 备忘录的这种设计思想是非常常见的&#xff0c;比如说围棋游戏的悔棋&#xff0c;绘图软件的撤销功能等等&#xff0c;都或多或少的使用了备忘录模式来处理对象的状态。 备忘录(Memento): 在不破坏封装性的前…

利用lay-ui结合ajax实现分页功能(不借助框架,简单易懂)

效果图: 1.创建html页面 01.html(前台文件) 2.创建index.php(后台文件) ------------------热身结束,开始正式分页之旅------------------ 3.在html页面中引入layui需要用到的css以及js,还有我们自己额外需要用到的jquery 4.在html文件中,将基本的分页栏显示出来 5.好啦,htm…

酷派手机android版本,系统版本迎来升级

系统版本迎来升级这个应该是两个版本之间最大但是却不那么直观的不同了&#xff0c;因为从TD版酷派大神F1采用的CoolLife UI 5.0版本&#xff0c;再到联通版酷派大神F1所搭载的CoolLife UI 5.5版本&#xff0c;它们之间经历了一个比较不错的升级。在图标ICON&#xff0c;功能设…

最终用户计算安全——特权访问控制

本篇算是系列的第二篇&#xff0c;之前写了一篇关于勒索软件攻击的&#xff0c;坦白说写这样的文很费脑子&#xff0c;而且喜欢看的读者估计也不多…不过我觉得整理一下思路&#xff0c;对于通过最终用户计算产品或方案来提升组织安全还是有很大的意义的。所以一边喝着清茶吃着…

详述 IntelliJ IDEA 插件的安装及使用方法

首先&#xff0c;进入插件安装界面&#xff1a; Mac&#xff1a;IntelliJ IDEA -> Preferences -> Plugins;Windows&#xff1a;File -> Settings -> Plugins.标注 1&#xff1a;显示 IntelliJ IDEA 的插件分类&#xff0c; All plugins&#xff1a;显示 IntelliJ …

杭漂两年,深漂两年,宇宙的尽头到底在哪儿

hi&#xff0c;这里是桑小榆。这次分享的是一位杭漂两年&#xff0c;深漂两年的码农伙伴的经历。首先他能够在大学期间就寻找到自己的热爱并持之以恒值得令人学习。其次他的工作经历可以说是非常的“程序员”&#xff0c;因为程序员所面对的职业生涯中&#xff0c;所谓的实习&a…

侣信即时通讯系统的技术解析

侣信&#xff1a; 说明&#xff1a; 侣信专业版是面向中小企业和者各类团队组织内部交流使用工具,可以在互联网或者局域网中使用。具有丰富的功能&#xff0c;聊天&#xff0c;群组&#xff0c;部门组织&#xff0c;内部朋友圈&#xff0c;以及漂流瓶摇一摇等功能。它可以在局域…

Confluence 6 使用 WebDAV 客户端来对页面进行操作

下面的部分告诉你如何在不同的系统中来设置原生的 WebDAV 客户端&#xff0c;这个客户端通常显示在你操作系统的文件浏览器中&#xff0c;例如&#xff0c;Windows 的 Windows Explorer 或者 Linux 的 Konqueror。在 Mac OSX Finder 中访问 Confluence你可以成功的连接&#xf…

.Net之接口小知识

目的通过一个简单的项目&#xff0c;在原来的文章基础上完善一下常用的几种WebApi编写方式以及请求方式&#xff0c;一方面是用于给我一个前端朋友用来学习调用接口&#xff0c;另一方面让我测试HttpClient的一些效果。本文示例代码环境&#xff1a;vs2022、net6准备新创建了一…

你所不知道的setTimeout

JavaScript提供定时执行代码的功能&#xff0c;叫做定时器&#xff08;timer&#xff09;&#xff0c;主要由setTimeout()和setInterval()这两个函数来完成。它们向任务队列添加定时任务。初始接触它的人都觉得好简单&#xff0c;实时上真的如此么&#xff1f;这里记载下&#…

android 特效绘图,Android绘图机制与处理技巧——Android图像处理之图形特效处理...

Android变形矩阵——Matrix对于图像的图形变换&#xff0c;Android系统是通过矩阵来进行处理的&#xff0c;每个像素点都表达了其坐标的X、Y信息。Android的图形变换矩阵是一个3x3的矩阵&#xff0c;如下图所示&#xff1a;72F0CAC1-14FB-40F8-A430-8F542B09DC4E.png当使用变换…

WPF 使用 DrawingContext 绘制刻度条

WPF 使用 DrawingContext 绘制刻度条控件名&#xff1a;Ruler作者&#xff1a;WPFDevelopersOrg原文链接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用大于等于.NET40&#xff1b;Visual Studio 2022;项目使用 MIT 开源许可协议&#xff1b;定义I…

个人中心的html,个人中心.html

&#xfeff;个人中心$axure.utils.getTransparentGifPath function() { return resources/images/transparent.gif; };$axure.utils.getOtherPath function() { return resources/Other.html; };$axure.utils.getReloadPath function() { return resources/reload.html; };…

使用CMD命令修改Windows本地账户密码

2019独角兽企业重金招聘Python工程师标准>>> 一、以管理员身份运行cmd命令 二、在命令提示符窗口中输入命令符&#xff1a;net user Administrator 123&#xff0c;然后按回车键“Enter”。(Administrator是你的win8用户名&#xff0c;123是重新设置的密码。) ​ 三…

java线程安全问题原因及解决办法

1.为什么会出现线程安全问题 计算机系统资源分配的单位为进程&#xff0c;同一个进程中允许多个线程并发执行&#xff0c;并且多个线程会共享进程范围内的资源&#xff1a;例如内存地址。当多个线程并发访问同一个内存地址并且内存地址保存的值是可变的时候可能会发生线程安全问…

html语言怎么添加图片,我想问你一下,你是怎么在html中插入本地图片?非常感谢...

满意答案小蜜蜂手工2013.10.03采纳率&#xff1a;43% 等级&#xff1a;12已帮助&#xff1a;7929人img{float:right}在下面的段落中&#xff0c;我们添加了一个样式为 float:right 的图像。结果是这个图像会浮动到段落的右侧。This is some text. This is some text. This i…

EntityFrameworkCore上下文如何实现继承?

【导读】如果我们存在基础设施服务和其他服务&#xff0c;我们会定义属于基础设施服务的上下文以及其他服务的上下文&#xff0c; 而且会独立部署&#xff0c;此时其他服务需要使用基础服务&#xff0c;我们都会暴露基础服务接口给到其他服务调用&#xff0c;这也是常规操作若在…

美观又实用,10 款强大的开源 Javascript 图表库

2019独角兽企业重金招聘Python工程师标准>>> 随着发展&#xff0c;现代 Web 设计在改善体验和功能的同时&#xff0c;对于美观的追求也越来越高&#xff0c;可视化、交互式、动态等元素和效果似乎已成为标配。 以下是为开发者推荐的 10 款开源 Javascript 图表库&am…