最近博客上一直在更新和C#无关的知识点,弄得我自己都以为自己自己要放弃C#了,不我还是那个对C#深爱着的小菜菜。(好恶心,哈哈哈)。
今天在知识梳理上,梳理一下C#中的泛型,希望一方面为C#语言的生态发展尽一份心力,也是在工作外也不放弃自己C#的功力基础。
1. 什么是泛型,在各种语言中泛型是什么?
第一种支持泛型的语言是ADA(一种Pascal语言的后代,差不多是军方的专用语言).而把泛型发扬光大的的是C++(主要是借助泛型实现的模板).
- C#,
- 作为本篇博客的主角,我们先说说C#中的泛型,在C#中泛型是C#2.0推出的新语法,不是语法糖,而是2.0由框架升级提供的功能。
- C#通过运行时检查来保证约束.
- C++
- 在C++中泛型的叫做“模板”
- C++的模板约束能力比较差.相当于没有.主要是通过代码替换.替换后的代码能便通过就通过了.通不过就报编译错误.不过未来的C++0X标准加入了模板约束.
- Delphi
- 在编译角度讲则更像C++,是通过代码替换机制生成参考类来实现泛型的.但Delphi的类型限制更严格.没有指定类型的约束的时候是不允许使用特定的方法或者操作符等的.具体在后面的约束中讲.
- Delphi通过编译器来保证泛型约束.
- Java
- Java中的泛型主要是通过编译器把泛型类全部变成object来实现的.而泛型类的方法是编译通过打包等技术进行类型转换的
例如List<T>实际上永远都是List<object>,不会生成新的参考类.
2. C#中的泛型是什么?
在C#中的泛型是什么?如下代码中就是泛型
List<T>;List<String>;List<Double>;List<Date>;List<UserDefaultClass>
public class UserDefaultClass
{
}
一般来说在C#语言中使用<>包含起来的就是泛型,也就是List,这个T就是泛型
泛型是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);
}
}
}
在上面的代码中我们会发现,我们对于每一种类型都要指定一种方法,所以为了解决这个问题大多数人会这样编写代码
- Object解决类型定义的问题
public static void ShowObject(object oParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}
但是在使用了上述的代码之后,会导致效率低下(频繁多次调用)的问题,原因是由于在调用是会有一次装箱,而在调用函数ShowObject时会将对象装箱到oParameter中,而在使用类型是会有一次拆箱操作。
- 装箱转换是指将一个值类型隐式地转换成一个object 类型,或者把这个值类型转换成一个被该值类型应用的接口类型interface-type。把一个值类型的值装箱,也就是创建一个object 实例并将这个值复制给这个object。
- 和装箱转换正好相反,拆箱转换是指将一个对象类型显式地转换成一个值类型,或是将一个接口类型显式地转换成一个执行该接口的值类型。
- 在CLR泛型出现之后可以这样写
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());
}
}
}
3. C#为什么会出现泛型?以及编译原理是什么?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
3.1 为什么泛型可以解决上面的问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
3.2 泛型究竟是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面一个例子:
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
从上面的截图中可以看出:泛型在编译之后会生成占位符。
注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。
4. C#中的泛型优秀在哪里?在C#中如何使用泛型。
4.1 C#中的泛型性能
请看一下的一个例子,比较普通方法、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();
结果:
4.2 除了方法可以是泛型以外,其他的也是可以泛型的
- 泛型类
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;
}
}
// 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);//泛型委托
TIP注意:
- 泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型,例如:
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>
{
}
}
5 泛型的5大约束
所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。
泛型约束总共有五种。
序号 | 约束名称 | 说明 |
1 | T:结构 | 类型参数必须是值类型 |
2 | T:类 | 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
3 | T:new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
4 | T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
5 | T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
- 基类约束
上面打印的方法约束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类没有子类。
- 接口约束
/// <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;
}
- 引用类型约束 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;
}
- 值类型约束 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;
}
- 无参数构造函数约束 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;
}
- 泛型约束也可以同时约束多个,例如:
注意:有多个泛型约束时,new()约束一定是在最后。
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();
}
6 泛型的协变与逆变
协变和逆变是在.NET 4.0的时候出现的,只能放在接口或者委托的泛型参数前面,out 协变covariant,用来修饰返回值;in:逆变contravariant,用来修饰传入参数。这也是最重要的。Java使用通配符支持协变性/逆变性。C#4受限的泛型可变性在一些程度上改善了一点,但Java的可变性模型仍然在一些时候要更好一些。
先看下面的一个例子:
定义一个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和List之间没有父子关系。
这时就可以用到协变和逆变了。
// 协变
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>();//逆变+协变
7 C#中使用泛型可以做什么“骚”操作
7.1 泛型缓存
在前面我们学习过,类中的静态类型无论实例化多少次,在内存中只会有一个。静态构造函数只会执行一次。在泛型类中,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);
}
}
}
}
7.2 泛型类实现枚举转换
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
namespace XC.Framework.Framework
{
/// <summary>
/// Enum帮助类
/// </summary>
/// <typeparam name="T"></typeparam>
public class EnumOperation<T>
{
static EnumOperation()
{
InitEnumInfo();
}
private static void InitEnumInfo()
{
DicEnumAndDes = new Dictionary<T, string>();
DicDesAndEnum = new Dictionary<string, T>();
DicValueAndEnum = new Dictionary<int, T>();
DicEnumAndValue = new Dictionary<T, int>();
string desc = "";
foreach (T pDataQueryWayType in Enum.GetValues(typeof(T)))
{
if (pDataQueryWayType.GetType().GetField(pDataQueryWayType.ToString()).IsDefined(typeof(XmlEnumAttribute), true))
{
var attrs = pDataQueryWayType.GetType().GetField(pDataQueryWayType.ToString()).GetCustomAttributes(typeof(XmlEnumAttribute), true);
if (attrs != null && attrs.Length > 0)
{
XmlEnumAttribute descAttr = attrs[0] as XmlEnumAttribute;
desc = descAttr.Name;
}
}
else
{
desc = pDataQueryWayType.ToString();
}
DicEnumAndDes.Add(pDataQueryWayType, desc);
DicDesAndEnum.Add(desc, pDataQueryWayType);
DicValueAndEnum.Add(Convert.ToInt32(pDataQueryWayType), pDataQueryWayType);
DicEnumAndValue.Add( pDataQueryWayType, Convert.ToInt32(pDataQueryWayType));
}
}
/// <summary>
/// 枚举字典信息--Key:枚举项;Value:枚举描述
/// </summary>
public static Dictionary<T, string> DicEnumAndDes = new Dictionary<T, string>();
/// <summary>
/// 枚举字典信息--Key:枚举描述;Value:枚举项
/// </summary>
public static Dictionary<string, T> DicDesAndEnum = new Dictionary<string, T>();
/// <summary>
/// 枚举字典信息--Key:枚举值(Int值);Value:枚举项
/// </summary>
public static Dictionary<int, T> DicValueAndEnum = new Dictionary<int, T>();
/// <summary>
/// 枚举字典信息--Key:枚举项;Value:枚举值(Int值)
/// </summary>
public static Dictionary<T, int> DicEnumAndValue = new Dictionary<T, int>();
}
}
7.3 泛型代码中的默认关键字:Default
在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T:
- T 是引用类型还是值类型。
- 如果 T 为值类型,则它是数值还是结构。
给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。解决方案是使用 default 关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。
namespace MyGeneric
{
class Program
{
static void Main(string[] args)
{
object obj1=GenericToDefault<string>();
object obj2 = GenericToDefault<int>();
object obj3 = GenericToDefault<StructDemo>();
Console.ReadKey();
}
public static T GenericToDefault<T>()
{
return default(T);
}
}
public struct StructDemo
{
public int Id { get; set; }
public string Name { get; set; }
}
}
大多数人不愿意烧脑,要的是流行歌曲,而非曲高和寡的咏叹调。