泛型(Generic)
本章主要详细讲解泛型的使用
文章目录
前言
泛型在C#编程的过程中会经常遇到,比如常见的就有,泛型集合,泛型方法,泛型类,泛型委托,泛型为我们编写代码提供了极大的便利,在此做此泛型专题,专门解读关于泛型的使用。
一、泛型(Generic)是什么?
1 官方对于泛型的概述
1 使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
2 泛型最常见的用途是创建集合类。
3 .NET 类库在System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应尽可能使用泛型集合来代替某些类,如 System.Collections 命名空间中的 ArrayList。
4 可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
5 可以对泛型类进行约束以访问特定数据类型的方法。
6 在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。
2 通俗理解
在编程的时候,经常会遇到功能相似的模块,只是处理的数据类型不同,如果分别写方法,觉得代码的重复太高,不写又没法实现功能,因此C#2.0推出了新语法泛型,它可以很好的解决上述问题,既可以最大限度的重用代码,又可以传入不同的数据类型进行逻辑处理。
二、为什么使用泛型
先看下如下案例:
需求:要求实现输入int ,string,datetime类型的值的时候,打印出对应的类型和值
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyGeneric
{
public class CommonMethod
{
//打印int的数据类型和值
public static void ShowInt(int a)
{
Console.WriteLine($"result:type={a.GetType().Name},value={a}");
}
//打印string的数据类型和值
public static void ShowString(string s)
{
Console.WriteLine($"result:type={s.GetType().Name},value={s}");
}
//打印DateTime 的数据类型和值
public static void ShowDateTime(DateTime dt)
{
Console.WriteLine($"result:type={dt.GetType().Name},value={dt}");
}
}
}
由上面案例可知,我们除了传入参数的数据类型不同,其余的处理逻辑相同,于是需要简化代码,
简化第一步:
使用Object,因为object 是一切类型的基类,于是有了以下代码:
// 打印输入参数的数据类型和值
public static void ShowObject(object o)
{
Console.WriteLine($"result:type={o.GetType().Name},value={o}");
}
问题:Object 是所有类型的父类,可以包容所有类型,因此可以实现需求,但是在这个过程中
存在数据类型转化,也就涉及到了装箱和拆箱,严重的影响程序的性能,因此C#2.0出现了泛型。
泛型可解决上述问题,这就是为什么我们需要使用泛型语法。
二、泛型类型参数
1 使用泛型类型参数
1 在泛型类型或方法定义中,类型参数是在其实例化泛型类型的一个变量时,客户端指定的特定类型的占位符。
2 泛型类(GenericList<T>)
无法按原样使用,因为它不是真正的类型;它更像是类型的蓝图。若要使用GenericList<T>
,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。
3 此特定类的类型参数可以是编译器可识别的任何类型。
4 可创建任意数量的构造类型实例,其中每个使用不同的类型参数。
如何理解上面的描述:
1 将泛型参数只当作是一个占位符,具体到底是什么类型,需要等到使用的时候指定才知道是什么类型
2 泛型类,比如MyGeneric<T>
不可能直接 MyGeneric<T> my=new MyGeneric<T>();
这样去使用,必须给他指定一个数据类型,如MyGeneric<int> my=new MyGeneric<int>();
3 T可以代表任何类型,也可识别任何类型。
4 可以这样示例 MyGeneric<int> my=new MyGeneric<int>();
也可以MyGeneric<string> my=new MyGeneric<string>();
只要你有需求,可以创建很多的实例
例如上面的案例中,就可以使用泛型方法编写如下:
//打印输入参数的数据类型和值
public static void ShowResult<T>(T t)
{
Console.WriteLine($"result:type={t.GetType().Name},value={t}");
}
使用注意:泛型方法的结构上和普通方法无异,只是在方法后面加上了尖括号,尖括号内放入泛型参数,业务中需要几个泛型参数,尖括号内就放几个。另外泛型参数一般用T表示,但是不代表不可以使用别的代表,也可使用V,K,或者其他自定义的名称,但是推荐命名的时候使用T或者T开头
2 泛型原理解析
1 为什么泛型可以解决上面的问题的?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
2 泛型是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面一个例子:
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//运行结果
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]
注意运行结果中的1,2 则表示上面案例中分别生成1个和2个占位符
3 泛型的性能如何?
实例可查看:泛型性能测试
官方描述:使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能
使用泛型是可以提高我们程序的运行性能的。
三、泛型适用对象
除了上面的泛型方法之外,还有以下对象
1 泛型类
public class MyGeneric<T>
{
public MyGeneric(T t)
{
Console.WriteLine($"result:type={t.GetType().Name};value={t}");
}
public void Show(T t)
{
Console.WriteLine($"result:type={t.GetType().Name};value={t}");
}
public void Test<V>(V v)
{
}
}
public class MyGeneric2<T,V>
{
public void Test2(T t,V v)
{
Console.WriteLine($"result:T={t.GetType().Name};V={v.GetType().Name}");
}
}
对以上代码有以下说明:
1 泛型类定义的时候与普通适用上基本相同,只不过类名后面多了个尖括号,尖括号中放了泛型参数用于占位
普通类
public class MyGeneric
//泛型类
public class MyGeneric<T>
2 注意区分和使用泛型类型中的普通方法和泛型方法
//普通方法
public void Show(T t)
//泛型方法
public void Test<V>(V v)
3 泛型类可以定义多个泛型参数
public class MyGeneric2<T,V>
调用案例中的方法
MyGeneric<int> myGeneric = new MyGeneric<int>(24);
myGeneric.Show(12131);
myGeneric.Test<string>("");
MyGeneric2<int, string> myGeneric2 = new MyGeneric2<int, string>();
myGeneric2.Test2(1,"22");
对以上代码有以下说明:
1 实例化泛型的时候,需要指明具体的数据类型,否则是无法实例化的
MyGeneric<int> myGeneric = new MyGeneric<int>(24);
2 指定之后,那么对于内部使用了泛型参数的方法只能传入对应的数据类型
Show方法指定输入T 类型的参数,当泛型类定义T为int的时候,那么Show方法也必须传入int类型参数
myGeneric.Show(12131);
3 可以根据自己的需求,创建不同的实例,
如MyGeneric<string> myGeneric = new MyGeneric<string>("24");
还有泛型接口和泛型委托,后续再进行完善
四.泛型约束
1 泛型约束概述
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 System.Object的成员,它是任何 .NET 类型的最终基类。 如果客户端代码使用不满足约束的类型,编译器将发出错误。
简单说:泛型约束就是让泛型类型参数在指定的数据范围内,如果不在指定范围内,编译器将会报错
2 泛型约束种类:
上图是全部的泛型约束。
通过使用Where关键字指定约束
下面描述一下常用的几种约束
1 where T : struct 表示类型参数必须是不可为null的值类型
public class My_Generic<T> where T : struct
{
public My_Generic(T t)
{
Console.WriteLine($"result:type={t.GetType().Name};value={t}");
}
}
使用该泛型类
My_Generic<int> my_Generic = new My_Generic<int>(1);
上面使用的时候,传入的是int,如果传入string 则会报错,因为string 是引用类型
如果约束定为struct,则只能传入 bool ,char,byte,short,int ,long,decimal,float,double等类型
2 where T : class 类型参数必须是引用类型。
public class My_Generic2<T> where T : class
{
public My_Generic2(T t)
{
Console.WriteLine($"result:type={t.GetType().Name};value={t}");
}
}
使用该泛型类
My_Generic2<string> my_Generic2 = new My_Generic2<string>("test");
My_Generic2<User> my_Generic22 = new My_Generic2<User>(new User());
由于约束定为class ,那么则可以传入object,string,或者实体类等类型
3 where T : new() 类型参数必须具有公共无参数构造函数。
与其他约束一起使用时,new() 约束必须最后指定。
public class My_Generic3<T> where T : new ()
{
public My_Generic3(T t)
{
Console.WriteLine($"result:type={t.GetType().Name};value={t}");
}
}
public class Score
{
public Score(string code)
{
//有参数的构造函数
}
}
#如果这样使用就会报错
My_Generic3<Score> my_Generic3 = new My_Generic3<Score>(new Score());
//这样使用则没有问题
My_Generic3<User> my_Generic3 = new My_Generic3<User>(new User());
4 where T :<基类名> 类型参数必须是指定的基类或派生自指定的基类。
where T :<接口名称> 类型参数必须是指定的接口或实现指定的接口。
public interface IPeople
{
void GetUserInfo();
}
public class Chinese : IPeople
{
public void GetUserInfo()
{
//throw new NotImplementedException();
}
}
//这个泛型类规定必须是IPeople或者是继承于Ipeople的数据类型才可传入
public class My_Generic4<T> where T : IPeople
{
public My_Generic4()
{
}
}
使用的时候
必须是IPeople或者是继承于Ipeople的数据类型才可传入
My_Generic4<IPeople> my_Generic4 = new My_Generic4<IPeople>();
My_Generic4<Chinese> my_Generic44 = new My_Generic4<Chinese>();
五.其他
1 泛型与default关键字
如上图,当需要给泛型类型的变量赋予初始值的时候,如果直接赋值就会报错,因为还没实例化,指定数据类型,
那么此时就可以用default。
使用default后会自动识别,如果传入进来的是值类型,就给值类型的默认值,如果是引用类型就给null
2 泛型与dynamic关键字
如上图,Add用于计算t1和t2之和的时候,直接使用int sum= t1+t2,会报错,因为还没有实例化,没有指定数据类型,无法直接适用于加法,但是如果使用dynamic,就可以跳过编译类型检查,改为在运行时解析这些操作。 就可以完成相关的业务逻辑。
总结
以上就是关于泛型的相关内容,本文仅仅简单介绍了泛型方法,泛型类以及泛型约束等的使用,后续会不断的更新本文章,完善泛型相关知识。