C# 泛型
泛型的使用
演化过程
- 没有泛型:多个参数类型的相同方法需要写多次。
public class CommonMethod
{
/// <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);
}
}
- 使用
Object
代替,解决重复的问题,但是引入装箱拆箱产生的性能问题和类型安全问题
public class CommonMethod
{
/// <summary>
/// 打印个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);
}
}
从上面的结果中我们可以看出,使用Object类型达到了我们的要求,解决了代码的可复用。
object类型的,为什么可以传入int、string等类型呢?
object类型是一切类型的父类。
通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。
但是上面object类型的方法又会带来另外一些问题:
装箱和拆箱,会损耗程序的性能。
类型安全问题。
于是微软在C#2.0的时候推出了泛型,可以很好的解决上面的问题。
- 使用泛型
public class GenericMethod
{
/// <summary>
/// <summary>
/// 泛型方法:方法名称后面加上尖括号,里面是类型参数
/// 类型参数实际上就是一个类型T声明,方法就可以用这个类型T了
/// </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);
}
}
泛型运行原理
1、为什么泛型可以解决上面的问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
2、泛型究竟是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。
泛型是类型安全的
开发一个用于在应用中传递对象的容器。但对象类型并不总是相同。因此,需要开发一个能够存储各种类型对象的容器。
鉴于这种情况,要实现此目标,显然最好的办法是开发一个能够存储和检索 Object 类型本身的容器,然后在将该对象用于各种类型时进行类型转换
public class ObjectContainer {
private Object obj;
public Object GetObj() {
return obj;
}
public void SetObj(Object obj) {
this.obj = obj;
}
}
虽然这个容器会达到预期效果,但就我们的目的而言,它并不是最合适的解决方案。它不是类型安全的,并且要求在检索封装对象时使用显式类型转换,因此有可能引发异常。
ObjectContainer myObj = new ObjectContainer();
// 存储一个 string
myObj.SetObj("Test");
Console.WriteLine("Value of myObj:" + myObj.GetObj());
// 存储一个 int (自动装箱为一个 Integer 对象)
myObj.setObj(3);
Console.WriteLine("Value of myObj:" + myObj.GetObj());
List objectList = new ArrayList();
objectList.add(myObj);
// 我们必须转换并且必须转换正确的类型以避免 ClassCastException!
string myStr = (string) ((ObjectContainer)objectList.get(0)).GetObj();
Console.WriteLine("myStr: " + myStr);
可以使用泛型开发一个更好的解决方案,在实例化时为所使用的容器分配一个类型,也称泛型类型,这样就可以创建一个对象来存储所分配类型的对象。泛型类型是一种类型参数化的类或接口,这意味着可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。
public class GenericContainer<T> {
private T obj;
public GenericContainer(){
}
// 将类型作为参数传递给构造函数
public GenericContainer(T t){
obj = t;
}
public T getObj() {
return obj;
}
public void setObj(T t) {
obj = t;
}
}
最显著的差异是类定义包含 <T>
,类字段 obj 不再是 Object 类型,而是泛型类型 T。类定义中的尖括号之间是类型参数部分,介绍类中将要使用的类型参数(或多个参数)。T 是与此类中定义的泛型类型关联的参数。
要使用泛型容器,必须在实例化时使用尖括号表示法指定容器类型。因此,以下代码将实例化一个 int 类型的 GenericContainer,并将其分配给 myInt 字段。
~·
GenericContainer
如果我们尝试在已经实例化的容器中存储其他类型的对象,代码将无法编译:
myInt.setObj(3); // OK
myInt.setObj("Int"); // Won't Compile
使用泛型的好处
上面的示例已经演示了使用泛型的一些好处。一个最重要的好处是更强的类型检查,因为避开运行时可能引发的 ClassCastException 可以节省时间。
另一个好处是消除了类型转换,这意味着可以用更少的代码,因为编译器确切知道集合中存储的是何种类型。
泛型的性能
对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。
值类型存放在栈上,引用类型存放在堆上。
ArrayListadd()
的时候,需要装箱,但是在读取ArrayList
的值的时候,需要进行拆箱。拆箱和装箱操作很容易,但是性能损失比较大,遍历很多项的时候更是如此。
但是 List
泛型类型参数
在泛型类型或泛型方法的定义中,类型参数是一个占位符(placeholder),通常为一个大写字母,如T。在客户代码声明、实例化该类型的变量时,把T替换为客户代码所指定的数据类型。
代码必须在尖括号内指定一个类型参数,来声明并实例化一个已构造类型(constructed type)。这个特定类的类型参数可以是编译器识别的任何类型。
泛型的默认值:有时候需要给泛型指定为 null,但是不能把 null 赋予泛型类型,因为泛型类型可以被实例化为值类型,而 null 只能用于引用类型,为了解决这个问题,可以使用default关键字,通过关键字,将 null 赋值给引用类型,将 0 赋值给值类型。
使用
泛型类
public class GenericClass<T>
{
public T _T;
}
class Program
{
static void Main(string[] args)
{
try
{
// T是int类型
GenericClass<int> genericInt = new GenericClass<int>();
genericInt._T = 123;
// T是string类型
GenericClass<string> genericString = new GenericClass<string>();
genericString._T = "123";
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
泛型接口
public interface IGenericInterface<T>
{
//泛型类型的返回值
T GetT(T t);
}
泛型委托
public delegate void SayHi<T>(T t);
//泛型委托
泛型类继承父类,实现接口
如果子类不是泛型的,那么继承的时候必须指定具体类型
/// <summary>
/// 使用泛型的时候必须指定具体类型,这里的具体类型是int
/// </summary>
public class CommonClass : GenericClass<int>
{
}
如果子类也是泛型的,那么继承的时候可以不指定具体类型
/// <summary>
/// 子类也是泛型的,继承的时候可以不指定具体类型
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonClassChild<T> : GenericClass<T>
{
}
如果子类不是泛型的,那么实现泛型接口的时候必须指定具体类型
/// <summary>
/// 必须指定具体类型
/// </summary>
public class Common : IGenericInterface<string>
{
public string GetT(string t)
{
throw new System.NotImplementedException();
}
}
如果子类也是泛型的,那么实现泛型接口的时候可以不指定具体类型
/// <summary>
/// 可以不知道具体类型,但是子类也必须是泛型的
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonChild<T> : IGenericInterface<T>
{
public T GetT(T t)
{
throw new NotImplementedException();
}
}
类型参数的约束
若要检查表中的一个元素,以确定它是否合法或是否可以与其他元素相比较,那么编译器必须保证:客户代码中可能出现的所有类型参数,都要支持所需调用的操作或方法。这种保证是通过在泛型类的定义中,应用一个或多个约束而得到的。
一个约束类型是一种基类约束,它通知编译器,只有这个类型的对象或从这个类型派生的对象,可被用作类型参数。一旦编译器得到这样的保证,它就允许在泛型类中调用这个类型的方法。上下文关键字where用以实现约束。
约束 | 描述 |
---|---|
where T: struct | 类型参数必须为值类型。 |
where T : class | 类型参数必须为引用类型。 |
where T : new() | 类型参数必须有一个公有、无参的构造函数。当于其它约束联合使用时,new()约束必须放在最后。 |
where T : | 类型参数必须是指定的基类型或是派生自指定的基类型。 |
where T : |
类型参数必须是指定的接口或是指定接口的实现。可以指定多个接口约束。接口约束也可以是泛型的。 |
where T : T2 | 类型参数 T 必须派生自泛型类型 T2。 |
只能为默认构造函数定义构造函数约束,不能为其他构造函数定义构造函数约束。
类型参数的约束,增加了可调用的操作和方法的数量。这些操作和方法受约束类型及其派生层次中的类型的支持。因此,设计泛型类或方法时,如果对泛型成员执行任何赋值以外的操作,或者是调用System.Object中所没有的方法,就需要在类型参数上使用约束。
无限制类型参数的一般用法
没有约束的类型参数,如公有类MyClass
- 不能使用运算符 != 和 == ,因为无法保证具体的类型参数能够支持这些运算符。
- 它们可以与System.Object相互转换,也可显式地转换成任何接口类型。
- 可以与null比较。如果一个无限制类型参数与null比较,当此类型参数为值类型时,比较的结果总为false。
无类型约束
当约束是一个泛型类型参数时,它就叫无类型约束(Naked type constraints)。当一个有类型参数成员方法,要把它的参数约束为其所在类的类型参数时,无类型约束很有用。