泛型generic使用和原理 封装泛型缓存
一.泛型的由来
泛型基本概念:.NET 泛型是一个很强大的新特性,它为每一种对象生成一份单独的代码(也就是所谓的“实例化”),这一份量身顶做的代码具有很高的效率,是强类型的,不需要运行期多态的支持和负担,有了泛型,就不再需要Object类来参与实现一些通用类或方法了.
泛型的作用: 在CLR(common language runtime)1.0中,当要创建一个灵活的类或方法,但该类或方法在编译期问不知道使用什么类,就必须以System.Object类为基础进行处理,而Object类在编译期间没有类型安全性,又必须进行强制类型转换.另外,给值类型使用Object类会有性能损失,这给程序开发带来诸多不便。故在CLR 2.0(.NET 3.5基于CLR 2.0)中,提供了泛型,创建的目的就是为了不同类型创建相同的方法或类,也包括接口,委托的泛型。
通过使用泛型类型,可以根据需要,用特定的类型替换泛型类型,同时保证了类型安全性:如果某个类型不支持泛型类,编译器就会报错,以阻止程度发生运行期错误.正确使用泛型将大大提高代码的灵活性,结合一个优秀的设计模式,可以显著缩短开发时间。
总而言之:泛型,其实就是 CLR+JIT的支持,使用泛型,可以重用代码,由CLR+JIT延迟等编译和执行的时候生成一个个具体的类或方法等等 的副本。比如:public class A类,这样,new的时候,var aStr= new A();var aInt=new A();这样,其实在编译的时候,会生成 A_001(string) 类和A_002 (int)类。
二、泛型的好处
值类型和引用类型的装箱拆箱消耗。值类型分配在线程栈上,引用类型分配在堆上,只把指针放在栈上。如图所示,如果把int类型1装箱,就要把1拷贝到堆中,就会有内存的交换。以前的ArrayList就是类型不安全的,需要频繁的进行装拆箱操作,Add元素的时候全部装箱object,取的时候要拆箱,性能损失比较大。
泛型的效率等同于硬编码的方式,就是和你很多功能相同的类效率差不多。泛型每个类型只实例化一次,下面泛型缓存会详细解读下。先简单介绍下CLR的运行原理以了解泛型的原理机制。
NET编译器和解释器两阶段,我们先经过编译器编译成IL中间语言(dll、exe),和java的字节码类似,然后经过JIT解释成机器码。这样做的好处就是我们只需要编译成IL后,在各个不同计算机系统上,只要有对应的CLR(JIT)就行,这样就和平台无关。二次编译:为了一次编译,不同平台使用。泛型在第一个编译时会用一个占位符代替,在第二次运行时会编译成具体的类型。所以性能相当于硬编码的方式,每种类型最终都有自己的机器码。
List是在使用时定义类型,JIT编译器解析时动态的生成,如定义List,在JIT运行时就声称List类型,然后操作就不会出现装箱拆箱,而且只能添加指定的类型,这就类型安全。
泛型与object的对比
比如我们需要打印出,传入参数的类型名称,那么使用常规代码方式,有常用2种方式去实现:
第一种,调用对应的showInt(),showString(),showTime()…方法,如下:
常规代码使用
/// <summary>
/// 打印个int值
/// </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);
}
//如果做数据库查询,每一个实体对应的表,那岂不是每一个实体都要写一个方法吗?
//看看调用:
int iValue = 123;
string sValue = "456";
DateTime dtValue = DateTime.Now;
object oValue = "789";
ShowInt(iValue);
ShowString(sValue);
ShowDateTime(dtValue);
第二种,使用showObject(object oParameter);
使用object
/// <summary>
///为什么用object 作为参数类型,调用的时候,可以把任何类型都传进来
///
///C#: 任何父类出现的地方 都可以用子类代替;
///Object类型是一切类型的父类
///
///Object 出现的都可以让任何类型传进来
///
/// 但是:有2个问题
/// 性能问题:会出现装箱和拆箱;
/// 类型安全问题。
/// </summary>
/// <param name="oParameter"></param>
public static void ShowObject(object oParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}
//看看调用:
ShowInt(iValue);
ShowString(sValue);
ShowDateTime(dtValue);
泛型类使用
/// <summary>
/// 泛型方法:需要在方法名的后面带一个<>,需要定义T, T是什么呢? 不知道,
/// T:类型参数,只是一个占位符,类型在声明其实不确定,在调用的时候,确定类型。
///
/// 延迟声明,推迟一切可以推迟的,事情能晚点做就晚点做。
/// 在使用的时候,需要指定明确的类型
/// 泛型方法性能和普通方法差不多,可以用一个方法满足多个类型的需求
/// 集成了前面2种的各自的长处。既可以安全调用,性能又好,并且可以用一个方法代替N个方法
/// </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(CommonMethod), tParameter.GetType().Name, tParameter);
}
//看看调用:
Show(iValue); //如果类型参数,可以通过参数类型推导出来,那么就可以省略
/* Show<int>(sValue);*/ //因为类型错了
Show(sValue);
Show<DateTime>(dtValue);
Show<object>(oValue);
为什么要有泛型,而不用object?
object类型:
- 1.任何父类出现的地方都可以用子类代替
- 2.object是一切类型的父类
object弊端:装箱拆箱性能损耗(栈的出栈和入栈受CLR管理)
int类型赋值之后存放在栈里面的,传递给Object类型时需要放到堆里面;使用的时候又需要从堆里面放到栈里面。
三、泛型各种应用
泛型类、泛型方法、泛型接口、泛型委托
泛型方法 一个方法满足多个类型的需求
泛型类 就是一个类 满足多个类型的需求
泛型接口 就是一个接口 满足多个多个类型的需求
泛型委托 就是一个委托 满足多个多个类型的需求
泛型类、泛型接口:创建方法类似,语法一样。用的最多的List就是很典型的泛型类,用来满足不同的具体类型,完成相同的事情。
泛型类:
//泛型类的定义在常规的类后面加<T>,T为占位符,可以任何字符代替。
//eg: public class Student<T>{}
//亦可以多个类型。占位符不重复,用逗号隔开
//eg: public class People<T,A,B,C>{}
//例子a:
public class ClassA<T>
{
/// <summary>
/// 获取T的类型全名称
/// </summary>
/// <returns></returns>
public string GetTTypeName()
{
return typeof(T).FullName;
}
}
//使用
//使用泛型来打印实例类型
{
ClassA<int> aInt = new ClassA<int>();
Console.WriteLine(aInt.GetTTypeName());//System.Int32
ClassA<string> aStr = new ClassA<string>();
Console.WriteLine(aStr.GetTTypeName());//System.String
Console.ReadLine();
}
//例子b:
public class ClassB<T,A>
where T:struct //约束T是个struct
where A:class,new () //约束A是个引用类型,无参数构造函数约束
{
/// <summary>
/// 获取T的类型全名称
/// </summary>
/// <returns></returns>
public string GetTTypeName()
{
return typeof(T).FullName;
}
}
泛型方法:常见的泛型方法就是在方法后面带上(T param),“T”可以随便定义,只要不是关键保留字就行,默认约定俗成都用T,此处就代表你定义了一个T类,然后后面参数就可以用这个T类型。(如果把鼠标光标放在参数类型T上,然后F12转到定义就会定位到前面这个T。)这样就可以用一个方法,满足不同的参数类型,去做相同的事情。把参数的类型申明推迟到调用时,延迟声明。后面框架中也会有很多这种延迟思想,延迟以达到更好的扩展。
public static void Show<T>(T tValue)
{
Console.WriteLine(tValue);
}
CommonMethod.Show<int>(123);
泛型接口:
//我们有4个类(人类、中国人、广东人、日本人),
//中国人继承人类,广东人继承中国人。日本人没继承人类。
//有2个接口(IWork,ISports)
public interface ISports
{
void Pingpang();
}
public interface IWork
{
void Work();
}
/// <summary>
/// 人类
/// 基本属性有:ID,名称,会说Hi
/// </summary>
public class People
{
public int Id { get; set; }
public string Name { get; set; }
public void Hi()
{ }
}
/// <summary>
/// 中国人:继承了人类(拥有人类的基本属性)
/// 也有自己的说Hi方式,有额外的属性,还实现了运动,工作
/// </summary>
public class Chinese : People, ISports, IWork
{
public void Tradition()
{
Console.WriteLine("仁义礼智信,温良恭俭让");
}
public void SayHi()
{
Console.WriteLine("吃了么?");
}
public void Pingpang()
{
Console.WriteLine("打乒乓球...");
}
public void Work()
{
Console.WriteLine("工作中...");
}
}
/// <summary>
/// 广东人:继承了中国人(拥有中国人的基本属性)
/// 额外还会吃。
/// </summary>
public class GuangDong : Chinese {
public void Eat()
{
Console.WriteLine("正在吃福建人...");
}
}
/// <summary>
/// 日本人:只实现了运动
/// </summary>
public class Japanese : ISports
{
public int Id { get; set; }
public string Name { get; set; }
public void Pingpang()
{
Console.WriteLine("雅美蝶,打乒乓球...");
}
}
//如下调用
People people = new People()
{
Id = 1,
Name = "令狐冲"
};
Chinese chinese = new Chinese()
{
Id = 2,
Name = "岳不群"
};
GuangDong guangDong = new GuangDong()
{
Id = 3,
Name = "小林子"
};
Japanese japanese = new Japanese()
{
Id = 4,
Name = "冲田杏梨"
};
//现在有这4个对象
//我想要他们,统统运动起来,如下:
//常规:
// people 没有运动的方法
chinese.Pingpang();
guangDong.Pingpang();
japanese.Pingpang();
//如果,我想他们统统运动起来,使用把对象传入进去的方法,
//那么又回到上面我说,替这3个类加3个方法
/*eg:
* public static void ChineseShowSports(Chinese chinese){ chinese.Pingpang(); }
* public static void GuangDongShowSports(GuangDong guangDong){guangDong.Pingpang(); }
* public static void JapaneseShowSports(Japanese japanese){japanese.Pingpang(); }
*/
//调用如下
ChineseShowSports(chinese);
GuangDongShowSports(guangDong);
JapaneseShowSports(japanese);
//这样也是可以的,那么下面的泛型就可以把他们泛型成一个方法
/*eg:
* public static void ShowSports<T>(T t) where T: People,ISports
* {
* t.Pingpang();
* }
*/
//调用如下
ShowSports(chinese);
ShowSports(guangDong);
ShowSports(japanese);
泛型委托:使用一个委托时,都需要先声明这个委托类,规定参数和返回值类型,然后才能实例化、调用。为了简化这个过程, .NET 框架为我们封装了三个泛型委托类,因此大部分情况下我们不必再声明委托,可以拿来直接实例化使用,方便了我们的日常Coding。
.这三种泛型委托包括:Func委托、Action委托和Predicate委托。
普通委托的定义:
public delegate int 委托名(int a, int b);
泛型委托的定义:
public delegate T 委托名<T>(T a, T b);
- Action委托代表返回值为空 void 的委托,它也有一些列重载,最多拥有16个输入参数。用法与Func相同。
- Func委托代表着拥有返回值的泛型委托。Func有一系列的重载,形式如 Func<T1,T2, … TResult>,其中TResult代表委托的返回值类型,其余均是参数类型。只有一个T时,即Func,代表该委托是无参数的。.NET封装了最多16个输入参数的Funct<>委托。
- Predicate委托,这个一般用的较少,它封装返回值为bool类型的委托,可被Func代替。
四、泛型的功能
泛型中的默认值
既然用了泛型,那么在内部想要初始化怎么办呢?因为泛型进来的类型不一定是值类型或引用类型,所以初始化就不能简单直接赋null。这个时候需要用到default关键字,用于将泛型类型初始化为null或其他值类型默认值(0,0001/1/1 0:00:00日期等)。
约束
泛型约束 是 在泛型类、方法、等对其调用的约束,起到保护作用和使用(履行义务+享受权利),使用方法是 后面跟 where T : Class,new()等。多重约束使用逗号隔开。
泛型导致任何类型都可以进来,那么如何去使用这个类型T,编写的时候我们是不知道T是什么,也不知道它能干什么。一个方法就是可以用反射,任何一个类型通过发射都能获取内部的结构属性方法调用。泛型约束提供更简便的方法。在声明泛型时在参数后面追加where关键字。约束可以同时指定多个,像这样where:T People,IWork,new()。同时约束传进来的类型People或其子类,并且继承了IWork接口,有无参数构造函数。
public static void Show<T>(T tValue) where T : People
{
Console.WriteLine(tValue.Name);
}
//多泛型如T,G,F,使用多个where。如:
public static void ShowSports<T,G,F>(T t)
where T :People,ISports
where G:ISports
where F:Class
{
//这里是业务,可以使用 T,G,F
}
说明:
where T:People// 约束T是People类型
where T:ISports// 约束T是实现ISports类型
where T :Class// 约束T是引用类型
where T :new()// 无参数构造的函数约束
where T :struct// 约束T是结构类型
协变逆变
协变逆变就是对参数和返回值的类型进行转换。协变用一个派生更大的类去代替某个类型(小代替大),其实就是设计原则的里氏替换原则,比如狗继承自动物,那么任何用动物作为参数类型的地方,调用时都可以用狗代替。逆变就是反过来。
//协变
public void ShowName(Animal animal)
{
}
ShowName(dog);
泛型接口的协变逆变。如果泛型类型用了out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。如果用了in关键字标注,就是逆变,只能把泛型类型T用作方法的输入。这块很绕,实际使用非常少。
//所谓协变逆变都是跟泛型相关
//只能放在接口或者委托的泛型参数前面
// out 协变 修饰返回值。就是让右边可以用子类
// out修饰之后只能做返回值,不能做参数
// in 逆变 修饰传入参数
//一堆狗肯定是一堆动物啊,为啥就不能这么做呢?下面这句编译不通过
//前后两个类型是没有父子关系的
List<Animal> animalLst = new List<Dog>();
//下面这句就可以呢?
IEnumerable<Animal> animalLst2 = new List<Dog>();
//因为在接口中添加了out关键字
public interface IEnumerable<out T> : IEnumerable
{
//
// 摘要:
// Returns an enumerator that iterates through the collection.
//
// 返回结果:
// An enumerator that can be used to iterate through the collection.
IEnumerator<T> GetEnumerator();
}
ICustomListIn<Dog> customLstIn = new CustomListIn<Animal>();
public interface ICustomListIn<in T>
{
void Show(T t);
}
public class CustomListIn<T> : ICustomListIn<T>
{
public void Show(T t)
{
Console.WriteLine(typeof(T).FullName);
}
}
interface ISetData<in T> //使用逆变
{
void SetData(T data);
}
interface IGetData<out T> //使用协变
{
T GetData();
}
class MyTest<T> : ISetData<T>, IGetData<T>//继承两个泛型接口
{
private T data;
public void SetData(T data)
{
this.data = data; //赋值
}
public T GetData()
{
return this.data; //取数据
}
}
MyTest<object> my = new MyTest<object>();
ISetData<string> set = my;
set.SetData("nihao");
其实协变逆变就是语法糖,为了让不是继承关系的类型也可以互相赋值编译通过。运行时实际右边是什么类型就是什么类型。
泛型缓存
泛型类的静态成员只能在类的一个实例中共享。运行时泛型类的实例已经指定了具体类型,每一个不同的泛型类实例共享静态成员,利用这个特点就可以做缓存。每一个不同的T缓存一个版本数据。如例子所示,当第一次指定不同的T时,会重新构造,再次有相同的类型时,就不会进入静态构造函数了。相当于为缓存了多个版本的静态成员。比如在各个数据库实体类需要有一些增删改查的SQL时,就可以利用用泛型特性,每一个数据库实体类都会缓存一份自己的增删改查SQL。
public class GenericCache<T>
{
static GenericCache()
{
Console.WriteLine("进入静态构造函数");
_TypeTime = $"{typeof(T).FullName}_{DateTime.Now.ToString()}";
}
private static string _TypeTime = "";
public static string GetCache()
{
return _TypeTime;
}
}
Console.WriteLine("************************");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(1000);
Console.WriteLine("认真比较打印出的静态成员值");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Console.WriteLine("************************");
五、总结
通过泛型类可以创建独立于类型的类,泛型方法创建出独立于类型的方法。接口、结构、委托也可以用泛型的方式创建。建议如果我们需要设计和类型无关的对象时,可以使用泛型,把锅甩给调用方,由上端决定实例化具体什么类型。