[.com原创稿件]泛型是一种编程语言风格,它允许程序员在实例化时使用一些后面指定的类型作为参数指定。泛型在.NET中使用尤其广泛。泛型是.NET2.0CLR中添加的新功能。它们类似于C++模板,但不如C++模板灵活,但它们也有自己的一些特点。泛型将类型参数的概念引入到.NET中,这样指定类型的工作就可以推迟到客户端代码声明并实例化类或方法时进行。接下来,我们来讲解一下泛型的知识。一、当C#没有泛型时在.NET2.0之前,开发者一直在使用System.Collections.Stack类,它是一个stack类型的集合对象。Stack通过Push和Pop方法向集合中添加和删除数据。通过前面的描述,很多开发者会认为使用Stack很简单,但是其中有一个很大的缺陷。Stack类存储对象类型,这使得CLR无法验证推入集合的对象是否是所需的类型。另外,我们在使用Pop方法的时候,需要将它的返回值转换成我们需要的类型,所以这里就有一个问题,如果Pop方法的返回值不是我们需要的类型,很有可能抛出异常。这里的返回值转换使用强制类型转换。由于使用强制类型转换在运行时执行类型检查,因此代码变得更加脆弱。使用Stack类时也存在性能问题。将值类型的实例传递给Push方法将在运行时对其执行装箱操作。频繁进行值类型装箱操作,系统会频繁分配内存和拷贝值。垃圾收集,这会导致大量的性能开销。通过前面的描述,读者应该看到Stack类不是类型安全的类,所以如果我们不使用泛型,如果我们修改Stack类并确保它是类型安全的,要求它存储指定的type,我们要这样做:publicclassStackDemo{publicvirtualUserPop();publicvirtualvoidPush(Useruser);//morecode}上面的代码是不是很简单?如果你真的这么想,那你想多了,因为我们只需要存储User类型的Formation,所以我们需要重写Stack的各个方法,如果我们还需要一个存储Student类型的Stack,我们需要重写每个再次Stack的方法。这就突出了一个问题,就是代码中会产生大量的相似代码和重复代码。另外,如果没有泛型,声明允许Null值的变量会比较麻烦。一般来说,我们常用两种方法。方法一:对于每一个需要处理空值的类型,都需要声明一个可为空的数据类型。我们来看一个简单的例子:structNullInt{publicintValue{get;privateset;}publicboolHasValue{get;privateset;}}上面的例子很简单,但是有两个问题。首先,如果我们有很多可空类型,我们需要编写很多类似的代码。其次,如果可空值类型发生变化,那么我们必须修改所有可能的类型声明。可想而知。体量巨大,也容易出错错误。方法二:该方法的出现就是为了解决我们在方法一中提到的两个问题,我们只需要声明一个可能的类型,它包含了对象类型的Value属性。先看一下代码:structNullType{publicobjectValue{get;privateset;}publicboolHasValue{get;privateset;}}这个方法完全解决了方法一存在的问题,但并不完美。因为在运行时设置Value属性时,值类型总是会被装箱,而通过NullType.Value获取值时需要强制类型转换,所以这个操作在运行时可能会报错。2.泛型概述泛型类型是在C#2.0中引入的。它的引入在一定程度上减轻了开发者的压力,也使得程序更加健壮和稳定。泛型类的语法也很简单,就是用尖括号来声明泛型类型参数和提供泛型类型实参。下面看一个定义泛型的例子:代码中,我们定义了一个泛型类来操作数据库。该类可供项目中所有需要操作数据库的类使用。我们只需要传入类型参数。比如我们需要往数据库中插入一条User数据。我们可以这样做://morecodeDataBaseOperatingdbOp=newDataBaseOperating();dbOp.Insert(user);//morecode我们看到在定义一个泛型类的时候,我是用T来定义类型参数的,这在C#开发中大部分是这样的人员的习惯,也可以说是大家默认的规范。我们在开发的时候一般会使用大写字母T作为前缀来表示它是一个类型参数。泛型的定义和用法这么多,是不是很简单?让我们解释一下泛型的各个方面。在学习泛型类之前,我们先了解一下它的优势,看看微软为什么要在C#2.0中引入泛型类。泛型促进类型安全,确保只能使用参数化类中成员明确期望的数据类型;类型检查发生在编译时,从而减少运行时无效转换的错误;通用类成员使用值类型,因此没有对象装箱转换。而且代码既保持了具体类的优点又避免了具体类的开销,所以代码的性能得到了提升,内存消耗变得很小。构造函数我们在开发中经常会用到构造函数,构造函数同样适用于泛型类和泛型结构体。泛型类/结构的构造函数与普通类/结构的构造函数完全相同。不需要类型参数,只需按照普通类/结构的构造函数的定义方法即可。publicclassDemo{publicTkey{get;set;}publicDemo(Tt){key=t;}}publicstructDemo{publicTvalue{get;set;}publicDemo(Tt){value=t;}}提示:构造函数包含类型参数,也可以在C#中,不仅有泛型类,还有泛型接口和泛型结构。通用接口和通用结构与通用类具有相同的语法。这里主要讲解如何在一个类中多次实现同一个泛型接口。我们先来看一下代码:publicinterfaceIDemo{ICollectionitems{get;set;}}publicclassDemo:IDemo,IDemo{ICollectionIDemo.items{get;set;}ICollectionIDemo.items{get;set;}}在上面的代码中,我们在类中显示了实现两种不同类型实参的同一个泛型接口。一般来说,在一个类中多次实现一个泛型接口并不是一个最优选择,因为这会造成代码混乱,在使用过程中造成误解。因此,除非有特殊情况,大多数情况下,我们不应该在一个类中多次实现同一个接口。默认值当我们需要在泛型类的构造函数中初始化一些属性,而其他属性没有初始化,但是在开发中我们无法确定传入泛型类的是什么类型的参数,所以我们无法通过具体的值来设置默认值。这种情况在C#中可以说是非常容易解决的,我们可以调用default运算符,为传入的任意类型的参数提供一个默认值。比如下面的代码中,我们只初始化了Key,而初始化值的使用默认运算符。publicclassDemo{TtKey{get;set;}TtValue{get;set;}publicDemo(Tkey){tKey=key;tValue=default(T);}}提示:default中的参数不用传入,在C#中7、如果可以推断出数据类型,则不需要指定参数。例如,Demodemo=default(T)可以写成Demodemo=default。多类型参数我们讲了单类型参数的泛型类,但是泛型类型不能只有一个参数,它可以有无限多个参数,比如我们定义一个泛型类,它的构造函数接受两个参数不同的类型,代码可以这样实现。publicclassDemo(){publicTKeykey{get;set;}publicTValuevalue{get;set;}publicDemo(TKeytKey,TValuetValue){key=tKey;value=tValue;}}我们在使用Demo时,只需要声明和实例化statementangle括号中指定的多个类型参数就足够了。调用时,提供与方法参数匹配的类型。Demodemo=newDemo(1,"小明");Console.Write($"number{demo.key}is{demo.value}")提示:在C#中,可以有多个具有相同名称但在同一名称空间中键入不同类的参数。在某些文章或书籍中,类型参数的数量称为arity。嵌套泛型嵌套泛型在开发中很少用到,但还是有必要在这里讲一下,因为有些开发者对这方面了解不多。嵌套泛型的外层也是泛型。外部泛型通常称为包含泛型。嵌套泛型类型会自动获取包含泛型类型的类型参数。这段话有点弯路,我来详细解释一下。比如A是一个包含泛型类型,它有一个类型参数T,B是一个嵌套泛型类型,它位于A中,那么B也可以使用A的类型参数T,如果B也包含一个类型参数T,那么B会隐藏A的类型参数T。这里需要提醒的是,如果嵌套的泛型类型的类型参数与被包含的泛型类型的类型参数相同,那么开发工具会有编译警告。这个警告是为了通知开发者使用了相同的类型参数,所以这里引出一个编码规则:避免在嵌套的泛型类型中使用同名参数来隐藏外部类型的类型参数。泛型方法我们之前说的就是泛型类。在C#中,除了泛型类之外,还有泛型方法。泛型方法的语法类似于泛型类,泛型方法不仅可以出现在泛型Type类中,也可以出现在普通类中。与泛型类相比,泛型方法有一个非常特殊的特点,就是泛型方法可以自己推断类型。编译器可以从传递给方法的实际参数中推断出泛型参数类型。因此,要使方法类型推断成功,实际参数类型必须与泛型方法的形式参数相匹配。3、泛型约束在开发中的大多数情况下,我们不允许任何不符合我们要求的类型参数出现在我们的代码中而导致错误。为防止出现此问题,您需要使用通用约束。声明通用约束需要where关键字后跟一对参数:requirements。这里的参数必须是声明在泛型类型中的参数,并且需要描述类型参数可以转换成的类或接口等条件。泛型约束分为:接口约束、类类型约束、类和结构约束、多重约束、构造函数约束。让我们一一解释。接口约束要指定一个数据类型必须是某个接口,需要声明一个接口类型约束。使用此约束可以避免转换调用显式接口成员的实现的需要。下面通过一个代码段来解释一下接口约束。publicclassDemowhereT:System.IComparable{//morecode}在上面的代码中,我们添加了System.IComparable约束,这意味着所有提供的类型参数都必须实现System.IComparable接口。那么当我们将StringBuilder作为类型参数传递给Demo创建Demo变量时,编译器就会报错,因为StringBuilder没有实现IComparable接口。类类型约束当我们需要将类型参数转换为特定类类型时,将使用类类型约束。类类型约束的语法与接口约束的语法相同。这里要注意一点,如果同时指定了多个约束,则类类型约束必须在第一位(最先出现),泛型约束中不允许有多个类类型约束,因为我们是代码不可能从您不想管理的多个类派生,并且类类型约束不能指定密封类或不是类的类型。class,structconstraintsclass和structconstraints是一个容易犯错误和混淆新手程序员的地方。首先,很多新手程序员看到类约束,会认为类型参数仅限于类类型,但事实并非如此。类约束是指类型参数是引用类型,所以这里使用的接口、类、委托、技术组类型都满足这个条件。结构约束与类约束正好相反。它将类型参数限制为值类型,并且值类型不能是可为空的值类型。因为可空值类型被实现为泛型NUllable,而NUllable中的T使用了结构约束。如果可以使用可控类型,那么NUllable很有可能被用作NUllableTip:由于类约束需要引用类型,结构约束需要值类型,所以这两种约束不能同时出现。多重约束我们可以为任何类型的参数指定任意数量的接口约束,所有的接口约束都需要用逗号分隔。如果有多个不同类型的约束,需要为每个约束写一个where关键字,不需要用任何符号来分隔不同类型的约束。下面看一下多约束代码段:publicclassDemowhereTKey:IA,IBwhereTValue:ClassA{//morecode}构造函数约束有时候我们需要在泛型类中创建一个类型参数的实例,那么我们可以指定泛型类传入的类型参数必须有一个构造函数。如果我们想实现这一点,我们可以使用new()作为限制。此约束称为构造函数约束。这里需要注意的是,构造函数约束必须位于所有约束的后面,它只能约束默认构造函数,不能约束参数构造函数。Tip1:关于约束继承的问题,想必很多开发者都一头雾水。这里我就用简单的几句话来说说约束继承。首先,泛型类型参数及其约束都不会被派生类继承,因为泛型类型参数和约束不是类的成员。虽然它不能被派生类继承,但是可以被从它派生的泛型类继承。由于派生的泛型类类型参数是泛型基类的类型参数,因此类型参数必须具有等于或强于泛型基类的约束。Tip2:泛型方法也可以使用约束,类似于泛型类。6.小结在这篇文章中,我主要讲解了泛型的一些知识。不能说面面俱到,但已经涵盖了90%的内容。泛型在开发中经常用到,善用泛型可以提高代码重用和程序性能。作者介绍朱刚,笔名喵大叔,国内知名技术社区博客认证专家,2019年度知名技术社区博客明星20强之一,.NET高级开发工程师,7年第一-线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于一家创业公司,从事企业级开发安全监控系统。【原创稿件,合作网站转载请注明原作者和出处为.com】
