C#泛型详解
这篇文章主要讲解C#中的泛型,泛型在C#中有很重要的地位,尤其是在搭建项目框架的时候。
一、什么是泛型
泛型是C#2.0推出的新语法,不是语法糖,而是2.0由框架升级提供的功能。
我们在编程程序时,经常会遇到功能非常相似的模块,只是它们处理的数据不一样。但我们没有办法,只能分别写多个方法来处理不同的数据类型。这个时候,那么问题来了,有没有一种办法,用同一个方法来处理传入不同种类型参数的办法呢?泛型的出现就是专门来解决这个问题的。
二、为什么使用泛型
先来看下面一个例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class CommonMethod { /// <summary> /// 打印个int值 /// /// 因为方法声明的时候,写死了参数类型 /// 已婚的男人 Eleven San /// </summary> /// <param name="iParameter"></param> public static void ShowInt(int iParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod).Name, iParameter.GetType().Name, iParameter); } /// <summary> /// 打印个string值 /// </summary> /// <param name="sParameter"></param> public static void ShowString(string sParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter); } /// <summary> /// 打印个DateTime值 /// </summary> /// <param name="oParameter"></param> public static void ShowDateTime(DateTime dtParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter); } } }
结果:
从上面的结果中我们可以看出这三个方法,除了传入的参数不同外,其里面实现的功能都是一样的。在1.0版的时候,还没有泛型这个概念,那么怎么办呢。相信很多人会想到了OOP三大特性之一的继承,我们知道,C#语言中,object是所有类型的基类,将上面的代码进行以下优化:
public static void ShowObject(object oParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod), oParameter.GetType().Name, oParameter); }
结果:
从上面的结果中我们可以看出,使用Object类型达到了我们的要求,解决了代码的可复用。可能有人会问定义的是object类型的,为什么可以传入int、string等类型呢?原因有二:
1、object类型是一切类型的父类。
2、通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。
但是上面object类型的方法又会带来另外一个问题:装箱和拆箱,会损耗程序的性能。
微软在C#2.0的时候推出了泛型,可以很好的解决上面的问题。
三、泛型类型参数
在泛型类型或方法定义中,类型参数是在其实例化泛型类型的一个变量时,客户端指定的特定类型的占位符。 泛型类(GenericList<T>
)无法按原样使用,因为它不是真正的类型;它更像是类型的蓝图。 若要使用GenericList<T>
,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。 此特定类的类型参数可以是编译器可识别的任何类型。 可创建任意数量的构造类型实例,其中每个使用不同的类型参数。
上面例子中的代码可以修改如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class GenericMethod { /// <summary> /// 泛型方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="tParameter"></param> public static void Show<T>(T tParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(GenericMethod), tParameter.GetType().Name, tParameter.ToString()); } } }
调用:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { class Program { static void Main(string[] args) { int iValue = 123; string sValue = "456"; DateTime dtValue = DateTime.Now; Console.WriteLine("***********CommonMethod***************"); CommonMethod.ShowInt(iValue); CommonMethod.ShowString(sValue); CommonMethod.ShowDateTime(dtValue); Console.WriteLine("***********Object***************"); CommonMethod.ShowObject(iValue); CommonMethod.ShowObject(sValue); CommonMethod.ShowObject(dtValue); Console.WriteLine("***********Generic***************"); GenericMethod.Show<int>(iValue); GenericMethod.Show<string>(sValue); GenericMethod.Show<DateTime>(dtValue); Console.ReadKey(); } } }
显示结果:
为什么泛型可以解决上面的问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
泛型究竟是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面一个例子:
Console.WriteLine(typeof(List<>)); Console.WriteLine(typeof(Dictionary<,>));
结果:
从上面的截图中可以看出:泛型在编译之后会生成占位符。
注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。
1、泛型性能问题
请看一下的一个例子,比较普通方法、Object参数类型的方法、泛型方法的性能。
添加一个Monitor类,让三种方法执行同样的操作,比较用时长短:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class Monitor { public static void Show() { Console.WriteLine("****************Monitor******************"); { int iValue = 12345; long commonSecond = 0; long objectSecond = 0; long genericSecond = 0; { Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 100000000; i++) { ShowInt(iValue); } watch.Stop(); commonSecond = watch.ElapsedMilliseconds; } { Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 100000000; i++) { ShowObject(iValue); } watch.Stop(); objectSecond = watch.ElapsedMilliseconds; } { Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 100000000; i++) { Show<int>(iValue); } watch.Stop(); genericSecond = watch.ElapsedMilliseconds; } Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}" , commonSecond, objectSecond, genericSecond); } } #region PrivateMethod private static void ShowInt(int iParameter) { //do nothing } private static void ShowObject(object oParameter) { //do nothing } private static void Show<T>(T tParameter) { //do nothing } #endregion } }
Main()方法调用:
Monitor.Show();
结果:
从结果中可以看出:泛型方法的性能最高,其次是普通方法,object方法的性能最低。
四、泛型类
除了方法可以是泛型以外,类也可以是泛型的,例如:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { /// <summary> /// 泛型类 /// </summary> /// <typeparam name="T"></typeparam> public class GenericClass<T> { public T _T; } }
Main()方法中调用:
// T是int类型 GenericClass<int> genericInt = new GenericClass<int>(); genericInt._T = 123; // T是string类型 GenericClass<string> genericString = new GenericClass<string>(); genericString._T = "123";
除了可以有泛型类,也可以有泛型接口,例如:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { /// <summary> /// 泛型接口 /// </summary> public interface IGenericInterface<T> { //泛型类型的返回值 T GetT(T t); } }
也可以有泛型委托:
public delegate void SayHi<T>(T t);//泛型委托
注意:
1、泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型,例如:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { /// <summary> /// 使用泛型的时候必须指定具体类型, /// 这里的具体类型是int /// </summary> public class CommonClass :GenericClass<int> { } }
如果子类也是泛型的,那么继承的时候可以不指定具体类型,例如:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { /// <summary> /// 使用泛型的时候必须指定具体类型, /// 这里的具体类型是int /// </summary> public class CommonClass :GenericClass<int> { } /// <summary> /// 子类也是泛型的,继承的时候可以不指定具体类型 /// </summary> /// <typeparam name="T"></typeparam> public class CommonClassChild<T>:GenericClass<T> { } }
2、类实现泛型接口也是这种情况,例如:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { /// <summary> /// 必须指定具体类型 /// </summary> public class Common : IGenericInterface<string> { public string GetT(string t) { throw new NotImplementedException(); } } /// <summary> /// 可以不知道具体类型,但是子类也必须是泛型的 /// </summary> /// <typeparam name="T"></typeparam> public class CommonChild<T> : IGenericInterface<T> { public T GetT(T t) { throw new NotImplementedException(); } } }
五、泛型约束
先来看看下面的一个例子:
定义一个People类,里面有属性和方法:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public interface ISports { void Pingpang(); } public interface IWork { void Work(); } public class People { public int Id { get; set; } public string Name { get; set; } public void Hi() { Console.WriteLine("Hi"); } } public class Chinese : People, ISports, IWork { public void Tradition() { Console.WriteLine("仁义礼智信,温良恭俭让"); } public void SayHi() { Console.WriteLine("吃了么?"); } public void Pingpang() { Console.WriteLine("打乒乓球..."); } public void Work() { throw new NotImplementedException(); } } public class Hubei : Chinese { public Hubei(int version) { } public string Changjiang { get; set; } public void Majiang() { Console.WriteLine("打麻将啦。。"); } } public class Japanese : ISports { public int Id { get; set; } public string Name { get; set; } public void Hi() { Console.WriteLine("Hi"); } public void Pingpang() { Console.WriteLine("打乒乓球..."); } } }
在Main()方法里面实例化:
People people = new People() { Id = 123, Name = "走自己的路" }; Chinese chinese = new Chinese() { Id = 234, Name = "晴天" }; Hubei hubei = new Hubei(123) { Id = 345, Name = "流年" }; Japanese japanese = new Japanese() { Id = 7654, Name = "werwer" };
这时有一个需求:需要打印出Id和Name属性的值,将ShowObject()方法修改如下:
但是这样修改报错了:object类里面没有Id和Name属性,可能会有人说,强制类型转换一下就行了啊:
public static void ShowObject(object oParameter) { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod), oParameter.GetType().Name, oParameter); Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}"); }
这样修改以后,代码不会报错了,这时我们在Main()方法里面调用:
CommonMethod.ShowObject(people); CommonMethod.ShowObject(chinese); CommonMethod.ShowObject(hubei); CommonMethod.ShowObject(japanese);
结果:
可以看出程序报错了,因为Japanese没有继承自People,这里类型转换的时候失败了。这样会造成类型不安全的问题。那么怎么解决类型不安全的问题呢?那就是使用泛型约束。
所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。
泛型约束总共有五种。
约束 | 说明 |
---|---|
T:结构 | 类型参数必须是值类型 |
T:类 | 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
T:new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
先看下面的示例代码:
class MyList<T> { List<T> list = new List<T>(); public T this[int i] { get { return list[i]; } set { this.list[i] = value; } } } class Person { public string Name { get; set; } }
1 指定泛型参数为值类型
class MyList<T> where T:struct { ...代码省略部分 }
看看加上约束后的效果怎么样,按下面方式 实例化MyList 类 :
MyList<Person> list = new MyList<Person>();
你会发现,将会产生下面的错误提示:
使用下面的方式实例化MyList类,将一切正常:
MyList<int> list = new MyList<int>();
如图所示:
2指定泛型参数为引用类型
class MyList<T> where T:class { ...代码省略部分 }
修改泛型约束为引用类型后,前面的错误提示消失,因为Person类是引用类型,满足泛型约束。
3指定泛型参数有无参的公共的构造函数
class MyList<T> where T:new() { ...代码省略部分 }
为Person类增加私有的无参构造函数,代码如下:
class Person { public string Name { get; set; } private Person() { //do nothing } }
实例化MyList<Person>类:
MyList<Person> list = new MyList<Person>();
出现编译错误,提示:
把Person类里面私有的构造函数去掉就可以了。
4指定泛型参数必须派生于指定基类
增加抽象类 SeniorAnimal
abstract class SeniorAnimal//高级动物 { public abstract void Speak();//会说话 public abstract void UseTool();//会使用工具 }
指定泛型参数 必须派生于基类 SeniorAnimal
class MyList<T> where T : SeniorAnimal { ...代码省略部分 }
实例化 MyList < Person > 类(此时Person类还未继承自 SeniorAnimal 类 ):
MyList<Person> list = new MyList<Person>();
出现编译错误,提示:
修改代码,使Person类继承自SeniorAnimal类:
class Person : SeniorAnimal { public string Name { get; set; } public override void Speak() { Console.WriteLine("我会说话!"); } public override void UseTool() { Console.WriteLine("我会用水果刀!"); } }
再次编译,一切正常。
5指定泛型参数必须实现指定接口
演示情况和第4点类似,就不提供演示代码了。
1、基类约束
上面打印的方法约束T类型必须是People类型。
/// <summary> /// 基类约束:约束T必须是People类型或者是People的子类 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="tParameter"></param> public static void Show<T>(T tParameter) where T : People { Console.WriteLine($"{tParameter.Id}_{tParameter.Name}"); tParameter.Hi(); }
注意:
基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用作约束就无任何意义,因为sealed类没有子类。
2、接口约束
/// <summary> /// 接口约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> public static T Get<T>(T t) where T : ISports { t.Pingpang(); return t; }
3、引用类型约束 class
引用类型约束保证T一定是引用类型的。
/// <summary> /// 引用类型约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> public static T Get<T>(T t) where T : class { return t; }
4、值类型约束 struct
值类型约束保证T一定是值类型的。
/// <summary> /// 值类型类型约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> public static T Get<T>(T t) where T : struct { return t; }
5、无参数构造函数约束 new()
/// <summary> /// new()约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> public static T Get<T>(T t) where T : new() { return t; }
泛型约束也可以同时约束多个,例如:
public static void Show<T>(T tParameter) where T : People, ISports, IWork, new() { Console.WriteLine($"{tParameter.Id}_{tParameter.Name}"); tParameter.Hi(); tParameter.Pingpang(); tParameter.Work(); }
注意:有多个泛型约束时,new()约束一定是在最后。
六、泛型的协变和逆变
协变和逆变是在.NET 4.0的时候出现的,只能放在接口或者委托的泛型参数前面,out 协变covariant,用来修饰返回值;in:逆变contravariant,用来修饰传入参数。
先看下面的一个例子:
定义一个Animal类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class Animal { public int Id { get; set; } } }
然后在定义一个Cat类继承自Animal类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class Cat :Animal { public string Name { get; set; } } }
在Main()方法可以这样调用:
// 直接声明Animal类 Animal animal = new Animal(); // 直接声明Cat类 Cat cat = new Cat(); // 声明子类对象指向父类 Animal animal2 = new Cat(); // 声明Animal类的集合 List<Animal> listAnimal = new List<Animal>(); // 声明Cat类的集合 List<Cat> listCat = new List<Cat>();
那么问题来了:下面的一句代码是不是正确的呢?
List<Animal> list = new List<Cat>();
可能有人会认为是正确的:因为一只Cat属于Animal,那么一群Cat也应该属于Animal啊。但是实际上这样声明是错误的:因为List<Cat>和List<Animal>之间没有父子关系。
这时就可以用到协变和逆变了。
// 协变 IEnumerable<Animal> List1 = new List<Animal>(); IEnumerable<Animal> List2 = new List<Cat>();
F12查看定义:
可以看到,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。
协变除了可以用在接口上面,也可以用在委托上面:
Func<Animal> func = new Func<Cat>(() => null);
除了使用.NET框架定义好的以为,我们还可以自定义协变,例如:
/// <summary> /// out 协变 只能是返回结果 /// </summary> /// <typeparam name="T"></typeparam> public interface ICustomerListOut<out T> { T Get(); } public class CustomerListOut<T> : ICustomerListOut<T> { public T Get() { return default(T); } }
使用自定义的协变:
// 使用自定义协变 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>(); ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();
在来看看逆变。
在泛型接口的T前面有一个In关键字修饰,而且T只能方法参数,不能作为返回值类型,这就是逆变。请看下面的自定义逆变:
/// <summary> /// 逆变 只能是方法参数 /// </summary> /// <typeparam name="T"></typeparam> public interface ICustomerListIn<in T> { void Show(T t); } public class CustomerListIn<T> : ICustomerListIn<T> { public void Show(T t) { } }
使用自定义逆变:
// 使用自定义逆变 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>(); ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();
协变和逆变也可以同时使用,看看下面的例子:
/// <summary> /// inT 逆变 /// outT 协变 /// </summary> /// <typeparam name="inT"></typeparam> /// <typeparam name="outT"></typeparam> public interface IMyList<in inT, out outT> { void Show(inT t); outT Get(); outT Do(inT t); } public class MyList<T1, T2> : IMyList<T1, T2> { public void Show(T1 t) { Console.WriteLine(t.GetType().Name); } public T2 Get() { Console.WriteLine(typeof(T2).Name); return default(T2); } public T2 Do(T1 t) { Console.WriteLine(t.GetType().Name); Console.WriteLine(typeof(T2).Name); return default(T2); } }
使用:
IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>(); IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆变+协变
七、泛型缓存
在前面我们学习过,类中的静态类型无论实例化多少次,在内存中只会有一个。静态构造函数只会执行一次。在泛型类中,T类型不同,每个不同的T类型,都会产生一个不同的副本,所以会产生不同的静态属性、不同的静态构造函数,请看下面的例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyGeneric { public class GenericCache<T> { static GenericCache() { Console.WriteLine("This is GenericCache 静态构造函数"); _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff")); } private static string _TypeTime = ""; public static string GetCache() { return _TypeTime; } } }
然后新建一个测试类,用来测试GenericCache类的执行顺序:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MyGeneric { public class GenericCacheTest { public static void Show() { for (int i = 0; i < 5; i++) { Console.WriteLine(GenericCache<int>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<long>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<DateTime>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<string>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<GenericCacheTest>.GetCache()); Thread.Sleep(10); } } } }
Main()方法里面调用:
GenericCacheTest.Show();
结果:
从上面的截图中可以看出,泛型会为不同的类型都创建一个副本,所以静态构造函数会执行5次。 而且每次静态属性的值都是一样的。利用泛型的这一特性,可以实现缓存。
注意:只能为不同的类型缓存一次。泛型缓存比字典缓存效率高。泛型缓存不能主动释放
到此这篇关于C#泛型详解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。