泛型(Generic)
文章目录
1.什么是泛型?
- 泛型是C#2.0推出的新语法,不是语法糖,而是2.0由框架升级提供的功能。
我们在编写代码时,经常会遇到功能非常相似的模块,只是他们处理的数据不一样(参数类),有没有一种方法用同一个方法来处理传入不同类型的参数呢?泛型就可以解决这个问题。
List<string> 就是泛型,带有"<>"括号的都是泛型,List<string>是一个集合,也有可能是一组 <int> 泛型集合。泛型的用处是用一个东西来满足不同类型的需求。
- 泛型命名空间
using System.Collections.Generic;
2.为什么使用泛型?
- 下面我们声明了两个方法,一个传入int值并打印,一个传入string值并打印,并进行了调用。
/// <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);
}
// 调用
int iValue = 123;
string sValue = "456";
CommonMethod.ShowInt(iValue);
CommonMethod.ShowString(sValue);
- 通过以上代码我们可以知道:因指定参数类型,只能传递某个类型,但是方法里实现的功能都是一样的,我们知道在C#语言里,Object是一切变量的基类,那我们是不是可以使用Object呢?
/// <summary>
/// 打印个object值
/// 1 任何父类出现的地方,都可以用子类来代替
/// 2 公理:object是一切类型的父类
/// </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);
}
//方法调用
int iValue = 123;
string sValue = "456";
CommonMethod.ShowObject(sValue);
CommonMethod.ShowObject(iValue);
-
但我们使用Object有两个问题:
-
1.装箱拆箱,性能损耗 传入一个int值(栈),object又在堆里面,如果把int传递进来,就会把值从栈里面copy到堆里面使用的时候,又需要用对象值,又会copy到栈(拆箱)。
-
2.会有类型安全问题,因为传递的对象是没有限制的。
-
注:.netframework 1.o版本原来使用这种方法,后续随着版本(2.0)的提升,推出了泛型这种更加安全的方式替换了这种Object。
3.泛型语法与思想
3.1 泛型方法与思想
/// <summary>
/// 泛型方法:方法名称后面加上尖括号,里面是类型参数
/// 类型参数实际上就是一个类型T声明,方法就可以用这个类型T了
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static void Show<T>(T tParameter)//, T t = default(T
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), tParameter.GetType().Name, tParameter);
}
方法调用
int iValue = 123;
string sValue = "456";
CommonMethod.Show<int>(iValue);//调用泛型,需要指定类型参数
CommonMethod.Show(iValue);//如果可以从参数类型推断,可以省略类型参数---语法糖(编译器提供的功能)
CommonMethod.Show<string>(sValue);
CommonMethod.Show<int>(sValue);//类型错了
-
通过上述代码我们思考下,为什么泛型可以解决上面的问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。 -
泛型究竟是如何工作的呢?
- 控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会* 把上面编译生成的占位符替换成具体的数据类型。
-
示例:
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
- 结果:
-
从上面的截图中可以看出:泛型在编译之后会生成占位符。
-
注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。
3.2 泛型类声明与使用
- 声明泛型类
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 namespace MyGeneric
8 {
9 /// <summary>
10 /// 泛型类
11 /// </summary>
12 /// <typeparam name="T"></typeparam>
13 public class GenericClass<T>
14 {
15 public T TProp{get;set;};
16 }
17 }
- Main()方法中调用:
// T是int类型
GenericClass<int> genericInt = new GenericClass<int>();
genericInt.TProp = 123;
// T是string类型
GenericClass<string> genericString = new GenericClass<string>();
genericString.TProp = "123";
3.4 泛型接口
/// <summary>
/// 泛型接口
/// </summary>
public interface IGenericInterface<T>
{
//泛型类型的返回值
T GetT(T t);
}
/// <summary>
/// 泛型类,实现泛型接口
/// </summary>
/// <typeparam name="T"></typeparam>
public class ClassModel<T> : IGenericInterface<T>
{
public T GetT(T t)
{
throw new NotImplementedException();
}
}
4.泛型的性能
-
看一下的一个例子,比较普通方法、Object参数类型的方法、泛型方法的性能。
-
添加一个Monitor类,让三种方法执行同样的操作,比较用时长短:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyGeneric
{
/// <summary>
/// 性能对比
/// </summary>
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
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyGeneric
{
/// <summary>
/// 性能对比
/// </summary>
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
}
}
- Main()方法调用:
Monitor.Show();
-
执行一亿次的结果,毫秒,结果:
-
从结果中可以看出:泛型方法的性能最高,其次是普通方法,object方法的性能最低。
5.泛型约束
5.1 约束汇总
约束 | 说明 |
---|---|
where T : struct | 类型参数必须是值类型 |
where T : class | 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
where T : new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
where T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
where T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
5.2 代码示例
- struct_值类型
class ClassModel<T> where T:struct
{
...代码省略部分
}
- class_引用类型
class ClassModel<T> where T:struct
{
...代码省略部分
}
-
其他类型命名方法与上面代码类似.
-
注意:
基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用作约束就无任何意义,因为sealed类没有子类。
6.泛型缓存与字典缓存
6.1 字典缓存
-
在实际的项目中,我们需要将用户的一些常用信息或常用的SQL语句进行缓存,这样大大减少了重复操作,下面是一个字典缓存例子:
/// <summary> /// 字典缓存 /// </summary> 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]; } }
-
在Main中调用
System.Console.WriteLine(DictionaryCache.GetCache<int>());
System.Console.WriteLine(DictionaryCache.GetCache<string>());
System.Console.Read();
- 结果
通过以上结果我们可以得知,字典缓存满足不了传入的类型不同,可以输出不同的结果,因为静态构造函数只会执行一次,静态属性常驻内存,而且字典缓存需要去内存堆里查找,执行也非常慢,并且也满足不了不同类型缓存不同内容,这时随着框架的升级就出现了泛型缓存。
6.2 泛型缓存
-
泛型缓存,众所周知,在C#里,静态构造函数只会执行一次,而在泛型类中,因为T的类型不同,每个的类型T,都会执行一次静态构造函数,所以会产生不同的静态属性和静态构造函数,例:
// 泛型缓存,使用场景:每个不同的泛型类 T,都会生成一份不同的副本,适合不同类型, // 需要缓存一份数据的场景,效率高。代码如下: // 声明泛型类 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; } }
- 在Main中调用
// 调用方法 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); }
结果如下,我们可以看到泛型缓存每当缓存不同类型的参数,都会执行一次构造函数,满足了缓存不同类型的要求,并且泛型缓存的执行速度也要大大超过字典缓存。
注:构造函数每次参数为不同类型时第一次都会调用,同一类型第二次调用不会执行构造函数,而是直接返回缓存的值。
7.泛型的协变逆变
7.1 为什么是泛型的协变与逆变呢?
- 协变与逆变是在.NET 4.0的时候出现的并且都是跟泛型相关, 只能放在接口或者委托的泛型参数前面。
7.2 协变_covariant
- 协变:就是让右边可以用子类,让泛型用起来更方便,关键字
out
修饰,协变后,T 只能作为返回值,不能当参数 - 在下面代码中:我们定义了一个鸟类,还定义了一个啄木鸟类,我们的啄木鸟继承了鸟这个类,并进行了实例化。
/// <summary>
/// 鸟类
/// </summary>
public class Bird
{
public int Id { get; set; }
}
/// <summary>
/// 啄木鸟类
/// </summary>
public class Woodpecker : Bird
{
public string Name { get; set; }
}
Bird bird1 = new Bird();//实例化鸟类
Bird bird2 = new Woodpecker();//子类实例化 啄木鸟当然是个鸟
- 那咱们的集合是不是也可以这样说呢:一堆的啄木鸟也是一堆鸟?语义上是没问题的,但是咱们的**
C#
的语法不通过,原因是List<Bird>
是一个类List<Woodpecker>
**也是一个类,没有父子关系,如果想要使用,我们只有强制转换一下。
List<Bird> birdList1 = new List<Bird>();
//不成立,语法不通过,原因是List<Bird>是一个类--List<Woodpecker>也是一个类,没有父子关系
List<Bird> birdList3 = new List<Woodpecker>();
//如果想使用,只能转换一下
List<Bird> birdList4 = new List<Woodpecker>().Select(c => (Bird)c).ToList();
-
但是这样是不是太麻烦了?上面代码证明了泛型还有不够和谐的地方,咱们泛型不就是为了书写方便,所以在.Net 4.0框架的时候,增加了协变与逆变,如下:
//IEnumerabl就是协变接口,并且是List的父类
//out修饰,协变后,T只能作为返回值,不能当参数
IEnumerable<Bird> birdList1 = new List<Bird>(); //实例化集合鸟类
IEnumerable<Bird> birdList2 = new List<Woodpecker>();//实例化集合啄木鸟类,父类是鸟类,可以正常使用
7.3 自定义协变
- 下面代码,定义了一个协变,**
ICustomerListOut<out T>
**客户列表泛型接口 与 **CustomerListOut<T> : ICustomerListOut<T>
**泛型客户列表类
/// <summary>
/// out 协变 只能是返回结果,不能做参数
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICustomerListOut<out T>
{
T Get();
//void Show(T t); 报错,只能是返回结果,不能做参数
}
public class CustomerListOut<T> : ICustomerListOut<T>
{
public T Get()
{
return default(T);
}
}
- 在Main中调用
ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();
ICustomerListOut<Bird> customerList2 = new CustomerListOut<Woodpecker>();
customerList2.Get();
注:通过上述代码可以看到,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类,这就是协变,让泛型更加方便使用。
7.4 逆变_contravariant
- 逆变:就是让右边可以用父类,让泛型用起来更方便,**
out
修饰,协变后,T
**只能作为返回值,不能当参数。
- 在Main方法中调用委托
Action<Woodpecker> act = new Action<Bird>((Bird i) => { }); // 子类 = 父类
7.5 自定义逆变
- 下面代码,定义了一个逆变,**
ICustomerListIn<in T>
**客户列表接口 与 **CustomerListIn<T> : ICustomerListIn<T>
**客户列表类
/// <summary>
/// 逆变
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICustomerListIn<in T>
{
//T Get();//不能做返回值,报错
void Show(T t);
}
public class CustomerListIn<T> : ICustomerListIn<T>
{
public void Show(T t)
{
}
}
- 在Main中调用
//逆变:就是让右边可以用父类,让泛型用起来更方便
//in修饰,逆变后,T只能作为当参数 不能做返回值,
ICustomerListIn<Woodpecker> customerList1 = new CustomerListIn<Bird>();
customerList1.Show(new Woodpecker());
注:通过上述代码可以看到,这时左边作为子类,右边可以用基类,这就是逆变,让泛型更加方便使用。
7.6 自定义协变+逆变
- 协变和逆变也可以同时使用,例:
/// <summary>
/// 泛型接口
/// </summary>
/// <typeparam name="inT">in 逆变,只能当参数</typeparam>
/// <typeparam name="outT">out 协变,只能当返回值</typeparam>
public interface IMyList<in inT, out outT>
{
/// <summary>
/// 声明了Show方法,参数为逆变inT
/// </summary>
/// <param name="t"></param>
void Show(inT t);
/// <summary>
/// 声明Get方法,无参数,返回值为逆变outT
/// </summary>
/// <returns></returns>
outT Get();
/// <summary>
/// 声明了Do方法,参数为逆变inT,返回值为协变outT
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
outT Do(inT t);
out 只能是返回值 in只能是参数
//void Show1(outT t);
//inT Get1();
}
/// <summary>
/// 泛型类,实现了IMyList<T1, T2>泛型接口
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
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);
}
}
- 在Main方法中调用
// (in逆变)子类与父类(out协变) = (in逆变)子类与父类(out逆变)
IMyList<Woodpecker, Bird> myList1 = new MyList<Woodpecker, Bird>(); //正常使用
// (in逆变)子类与父类(out协变) = (in逆变)子类与子类(out逆变)
IMyList<Woodpecker, Bird> myList2 = new MyList<Woodpecker, Woodpecker>();//协变
// (in逆变)子类与父类(out协变) = (in逆变)父类与父类(out逆变)
IMyList<Woodpecker, Bird> myList3 = new MyList<Bird, Bird>();//逆变
// (in逆变)子类与父类(out协变) = (in逆变)父类与子类(out逆变)
IMyList<Woodpecker, Bird> myList4 = new MyList<Bird, Woodpecker>();//协变+逆变
//协变out:左边是父类,右边是子类,只能当参数,无法当返回值
//逆变in :左边是子类,右边是父类,只能当返回值,无法当参数
注:协变和逆变在C#中通常用于事件处理(其中事件处理程序可以处理派生类的事件)和LINQ(其中协变和逆变允许更灵活的类型转换)。在接口中使用它们可以增强代码的灵活性和重用性。然而,需要注意的是,不是所有的泛型接口或方法都可以使用协变和逆变,它们的使用受到一定的限制,并且通常只在特定的上下文(如委托和接口)中才有意义。