主要内容概要
- 引入泛型:延迟声明
- 如何声明和使用泛型
- 泛型的好处和原理
- 泛型类、泛型方法、泛型接口、泛型委托
- 泛型约束
- 协变 逆变(选修)
- 泛型缓存(选修)
为什么要有泛型
很常见的比如List
List
List
泛型就是用一个东西来满足多种不同类型的需求的。
下面举一个例子来说明一下,为何泛型会被引入。
在CommonMethod里面有三个方法,分别打印三种不同的类型,如下:
public class CommonMethod
{
// 打印个int值
public static void ShowInt(int iParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod).Name, iParameter.GetType().Name, iparameter);
}
//打印个string值
public static void ShowString(string sParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);
}
//打印个DateTime值
public static void ShowDateTime(DateTime dtParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);
}
}
那么在program中我们就可以这么来调用这三种方法:
int iValue = 123;
string sValue = "456";
DateTime dtValue = DateTime.Now;
object oValue = "789";
CommonMethod.ShowInt(iValue);
CommonMethod.ShowString(sValue);
CommonMethod.ShowDateTime(dtValue);
这个写法是不是感觉有点啰嗦呢,三个方法其实只有传入的参数不一样而已。此时,我们可能会考虑在common中这么来写一个函数,将object对象作为传入参数:
public static void ShowObject(object oParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}
然后在program中可以这么来调用:
CommonMethod.ShowObject(oValue);
CommonMethod.ShowObject(iValue);
CommonMethod.ShowObject(sValue);
CommonMethod.ShowObject(dtValue);
为什么用object 作为参数类型,调用的时候,可以把任何类型都传进来?
- C#: 任何父类出现的地方,都可以用子类代替
- Object类型是一切类型的父类;
这里使用object会有两个问题:
- 第一是装箱拆箱,传入一个int值(栈)
object又在堆里面,如果把int传递进来,就会把值从栈里面copy到堆里,使用的时候,又需要用对象值,又会copy到栈(拆箱); - 类型安全问题,可能会有,因为传递的对象是没有限制的;
泛型简介
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性。泛型为.Net框架引入了类型参数(type parameters)的概念。类型阐述是的设计类和方法时,不必确定一个或多个具体参数,其具体参数可以延迟到客户代码中声明、实现。这意味着使用泛型的类型参数T,写一个类MyList
*泛型只有:泛型方法、泛型类、泛型接口、泛型委托 *
泛型方法调用的时候,需要加上<>,而且需要指定具体的类型、指定的类型和传入的参数类型保持一致。
定义一个泛型方法:
public static void Show<T,S,ZCQ>(T tParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), tParameter.GetType().Name, tParameter);
}
然后调用泛型方法:
CommonMethod.Show<int>(iValue);
CommonMethod.Show(iValue); //如果类型参数,可以通过参数类型推导出来,那么就可以省略,VS中会变灰
// 下面的例子中类型错了
//CommonMethod.Show<int>(sValue);
CommonMethod.Show(sValue);
CommonMethod.Show<DateTime>(dtValue);
CommonMethod.Show<object>(oValue);
思考一下,为什么泛型可以支持不同类型的参数?
声明一般方法时,指定了参数类型,确定了只能传递某个类型;
泛型声明方法时,并没有写死类型,是什么类型,不知道;
只有在调用的时候才指定;
泛型这里的设计思想是延迟声明,这是架构设计推崇的思想:推迟一切可以推迟的!
泛型原理
编译器编译(例如VS),得到一些dll exe这种文件,这些文件要运行需要CLR/JIT进行转换(这个CLR是个环境),这个CLR会把中间语言转换成机器语言。
那么,泛型在编译时究竟编译成什么呢?
机器码的时候,类型必须是确定的,因为要分配内存;
编译时确实不知道是什么类型。
那么泛型在编译时,延迟声明究竟是怎么实现的呢?
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
上面的输出结果如下:
System.Collections.Generic.List1[T]
System.Collections.Generic.Dictionary
2[TKey,TValue]
通过结果可以发现,所谓延迟声明实际上是通过占位符来实现的。
泛型是.NetFramework2.0出来
包含了以下升级:
- 编译器升级,能够支持类型参数,用占位符来表达
`1、`2来表示 - CLR升级才能支持占位符
运行的时候,类型会确定,会把占位符给替换成具体的类型 - 泛型不是语法糖* (语法糖是指编译器提供的便捷功能,例如 var i=2,编译器可以推断出来是int类型。)
泛型性能
下面比较一下泛型方法,object方法以及一般方法的性能:
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 < 100_000_000; i++)
{
ShowInt(iValue);
}
watch.Stop();
commonSecond = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 100_000_000; i++)
{
ShowObject(iValue);
}
watch.Stop();
objectSecond = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 100_000_000; 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
}
得到的结果是:commonSecond=363,objectSecond=696,genericSecond=362
可以发现泛型方法的性能和普通方法的性能是一致的,在一个数量级上,而object需要在内存中装箱,因此消耗的时间比较多。
泛型缓存
字典缓存
字典缓存:静态属性常驻内存public class DictionaryCache { private static Dictionary<Type, string> _TypeTimeDictionary = null; static DictionaryCache() { Console.WriteLine("This is DictionaryCache 静态构造函数"); _TypeTimeDictionary = new Dictionary<Type, string>(); } public static string GetCache<T>() { Type type = typeof(Type); if (!_TypeTimeDictionary.ContainsKey(type)) { _TypeTimeDictionary[type] = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff")); } return _TypeTimeDictionary[type]; } }
泛型缓存
会根据传入的不同类型,分别生成不同的副本。虽然是静态字段,但是遇到泛型类,也是不同的。
可以接受任何类型,需要根据不同类型缓存一部分数据就可以使用,效率更好。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; } }
验证一下:第一次调用int string long等类型的时候会产生一个类,第二次的时候就不会进入构造函数了,这样就达到了类型缓存的目的。
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);
}
}
}
泛型缓存比字典缓存效率要高,字典缓存要用哈希,转换效率低。但是这种泛型缓存只能是为某一个类型进行缓存,有一定的局限性。
泛型的应用
- 泛型方法
泛型方法:为了一个方法满足不同的类型的需求
- 一个方法完成多实体的查询
- 一个方法完成不同的类型的数据展示
- 任意一个实体,转换成一个Json字符串
泛型类
泛型类:一个类型,满足不同类型的需求;List Dictionarypublic class GenericClass<T> { }
泛型接口
泛型接口:一个接口满足不同类型的需求public interface GenericInterface<T> { }
泛型委托
泛型委托:就是一个委托 满足多个多个类型的需求public delegate void Do<T>();
泛型类、接口的继承,也要遵从相应的规则,例如:
public class ChildClass<S, T> : GenericClass/<S/>, GenericInterface<T>
泛型约束
先看一个例子用object有什么样的弊端?任何类型都能传递进来可能不安全。
public static void ShowObject(object oParameter)
{
//Console.WriteLine(oParameter.Id);// 编译器就报错,因为C# 是强类型的语言,在编译的时候就要确定类型
People people = (People)oParameter;
Console.WriteLine(people.Id);
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}
下面再看看泛型约束:
先定义一些类,接口,有人,日本人,中国人,湖北人:
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()
{ }
}
public class Chinese : People, ISports, IWork
{
//...
}
public class Hubei : Chinese
{
//...
}
public class Japanese : ISports
{
//...
}
可以定义一些泛型约束:
where T: BaseModel
基类约束;
- 可以把T当成基类——权利
- T必须是People 或者 其子类——约束
public static void GenericShow<T>(T tParameter) where T : People { Console.WriteLine("This is {0},parameter={1},type={2}", typeof(CommonMethod), tParameter.GetType().Name, tParameter); }
GenericConstraint.Show(people); GenericConstraint.Show(chinese); GenericConstraint.Show(hubei); GenericConstraint.Show(japanese);//这里就会报错, Japanese传不进来
泛型约束类型有:
基类约束
where T : People
接口约束
where T : ISports
引用类型约束,值类型约束
public T GenericShow<T>() where T: class { return null;//引用类型 }
public T GenericShow<T>() where T: struct { return default(T);//值类型,不约束也可以这么用 }
无参数构造函数约束
public static void GenericShow<T>(T tParameter) where T : new()// 无参数构造偶函数约束 { return new T(); }
- 约束中,密封的是不行的,没有子类就谈不上约束了,例如where T: string 是不对的!
- 约束的时候,父类只有一个,接口可以多个
- 泛型约束可以结合使用 ,用逗号分隔就OK
public static void GenericShow1<T>(T tParameter) where T : class, new() // 泛型约束可以结合使用 ,用逗号分隔就OK { //... } public static void GenericShow1<T,S>(T tParameter) where T : class, new() where S :People { //... }
- 用泛型类型的参数来约束
public static void ShowTS<T,S>(T tParameter,S sParameter) where T: People where S: T//用类型参数约束 { //... }
泛型的协变与逆变
Func<int,string>这是一个泛型委托,来看看这个定义:public delegate TResult Func<in T, out TResult>(T arg)
简单理解,这个里面的 in 和out 就是协变与逆变。
下面举一个例子来具体阐述一下:
public class Bird
{
public int Id { get; set; }
}
public class Sparrow : Bird
{
public string Name { get; set; }
}
Bird bird1 = new Bird();//子类实例化 麻雀当然是个bird
Bird bird2 = new Sparrow();
Sparrow sparrow1 = new Sparrow();
/* Sparrow sparrow2 = new Bird()*/ //子类变量 不能用父类实例化
思考: 一个麻雀是一个bird,难道一组麻雀不是一组鸟吗?语义应该是可以的。
但是语法上是通不过的,List
如果非要这么实现一下,可以这么写:List<Bird> birdList3 = new List<Sparrow>().Select(c => (Bird)c).ToList();
至此,会感觉泛型在使用的时候,会存在不和谐的地方。
更进一步,我们看一下,不用List声明,用IEnumerable来实现上例。可以这样写:
IEnumerable<Bird> birdList1 = new List<Bird>();
IEnumerable<Bird> birdList2 = new List<Sparrow>();
为什么可以这么写呢?探究一下IEnumerable和List:
这里面IEnumerable的参数就是个out类型,修饰返回值,就是协变covariant。
先给出协变逆变的几个结论:
- 协变逆变只有在接口或者委托的泛型参数前面用 out in来实现
- out 协变covariant 修饰返回值
- in 逆变contravariant 修饰传入参数
- 下面自己来写一个协变的实例:
public interface ICustomerListOut<out T>
{
T Get();
//void Show(T t);
}
public class CustomerListOut<T> : ICustomerListOut<T>
{
public T Get()
{
return default(T);
}
//public void Show(T t) { }
}
上述ICustomerListOut这个接口,用out修饰,就是协变,意味着T就只能做返回值 ,不能做参数。也就是说,不能再接口中定义void Show(T t);
这样的方法,同样也不能在实现类中把T作为参数使用。
这样一来,通过协变,就可以像下面这样,可以把子类放在右边,这才是泛型该有的样子。
ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();
ICustomerListOut<Bird> customerList2 = new CustomerListOut<Sparrow>();
我们再来想一下这个过程,bird在ICustomerListOut
如果在上述过程中,这个T可以作参数来用,例如我们有这么一个方法:void Show(T t);
,这样,在接口ICustomerListOut
- 下面再看一下逆变的实例:
public interface ICustomerListIn<in T>
{
//T Get();
void Show(T t);
}
public class CustomerListIn<T> : ICustomerListIn<T>
{
//public T Get()
//{
// return default(T);
//}
public void Show(T t)
{
}
}
与协变相反,逆变是用in作修饰符,泛型T只能作输入参数,不能作返回值,可以让右边使用父类
ICustomerListIn<Sparrow> customerList2 = new CustomerListIn<Sparrow>();
ICustomerListIn<Sparrow> customerList1 = new CustomerListIn<Bird>();
customerList1.Show(new Sparrow());
ICustomerListIn<Bird> birdList1 = new CustomerListIn<Bird>();
birdList1.Show(new Sparrow());
birdList1.Show(new Bird());
再来想一下上面这个过程:对customerList1来说,其实现类CustomerListIn<Bird>()
会把Bird作为参数在方法public void Show(T t)
中使用,所以这个时候传入Sparrow没问题,因为Sparrow也是bird。如果T作为返回值使用,那么接口中的T范围比实现类中的T范围大,就无法正常工作了。
对于birdList1来说,实现类和接口中T都是Bird,只会作参数使用,所以birdList1.Show(new Sparrow())中,传入Sparrow没有问题。
- 最后再看一个协变逆变一起的实例:
public interface IMyList<in inT, out outT>
{
void Show(inT t);
outT Get();
outT Do(inT t);
////out 只能是返回值 in只能是参数
//void Show1(outT t);
//inT Get1();
}
实现类如下:
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<Sparrow, Bird> myList1 = new MyList<Sparrow, Bird>();
myList1.Show(new Sparrow());
IMyList<Sparrow, Bird> myList2 = new MyList<Sparrow, Sparrow>();//协变
myList2.Show(new Sparrow());
IMyList<Sparrow, Bird> myList3 = new MyList<Bird, Bird>();//逆变
myList3.Show(new Sparrow());
IMyList<Sparrow, Bird> myList4 = new MyList<Bird, Sparrow>();//协变+逆变
myList4.Show(new Sparrow());
上述过程这里不再赘述,就是交叉着使用协变逆变,应该比较好理解了。