泛型Generics
泛型是什么
所谓泛型,通用类型的意思,英文单词是generics,可以理解为,以类型为参数,在原有类型或方法上进一步的抽象。泛型的作用
- 避免了装箱拆箱造成的资源消耗
- 是类型安全的
- 提高代码复用
泛型的主要思想就是将算法与数据结构完全分离开来,使得一次定义的算法能够作用于多种数据结构,从而实现高度可重用的开发。
通过泛型可以定义类型安全的数据结构,而没有必要使用实际的数据类型。这将显著提高性能并得到更高质量的代码,因为可以重用数据处理算法,而没有必要复制类型特定的代码。如何实现一个泛型
泛型类:class classname<T>{}
泛型方法:
T methodname<T>(){
T res=default(T);
return res;
}
泛型委托:Action<T1>/Func<T1,TResult>
泛型接口:IList<T>
和普通类一样,泛型类可以拥有各种成员,包括非静态和静态的字段、方法、构造器、索引函数、委托和事件等。在泛型类的成员中可以自由地使用类型参数来指代数据类型,包括用来定义字段类型和方法成员的返回类型。传递给方法成员的参数类型,以及在方法成员的执行代码中定义局部变量的类型。
类型参数在作为传递给方法的参数使用时,既可以作为一般参数,也可以作为引用参数和输出参数,甚至可以是数组参数。泛型约束
- 引用类型约束
一般使用where语句接在<T>
或<T>()
之后花括号之前,来限定T只能是引用类型
格式:类型—XXX <T> where T:class {}
- 值类型约束
与引用类型约束类似 ,指定了值类型约束后,类型实参只能是结构值类型。
格式为T:struct
- 构造函数约束
用来检查类型实参是否具有用于创建类型实例的无参数构造函数。
格式为T:new ()
- 转换类型约束
作用是进一步限定T的类型只能是某个类或继承自某个类或实现了某些接口的类。
格式为T:[typename]
多个约束的组合使用”,”隔开
如where T:class,IList,new ()
注意:- class、struct、其他指定类类型 ,不能同时存在
- struct与new ()不能同时存在
- 多个约束组合要按优先级顺序来定义约束 class或struct或类类型 > 接口类型> new ()
多个类型参数直接where跟在后面
如<T,F> where T:struct where F:class
- 引用类型约束
泛型方法
- 基本用法,格式为:XXX(){}
使用类型参数T后,在语句块中就可以使用对应类型T了,其默认值为default(T)
如:
void DoSomething<T>(){
T res=default(T);
} - 泛型方法本身的参数,返回类型也都可以使用泛型类型T
如:
T DoSomething<T>(T arg1,T arg2){
T res=default(T);
return res;
}
对于泛型方法拥有参数的情况,泛型机制提供了类型推断:
当泛型方法的参数中有至少一个类型参数对应的类型T时,便会进行类型推断,如果条件符合则不必写出<T>
- 方法参数一个T时:如
void Do<T>(T arg){};------>调用 Do("abc");
- 方法参数多个T时:如
void DoSomething<T>(T arg1,T arg2)------>调用 DoSomething("abc","cba");
如果DoSomething("abc",123);
则不被允许,此时明显T所绑定的类型不一致。
但是arg1与arg2之间可以是派生关系,遵循逆变性,如:
DoSomething(new UnityEngine.Object(), new UnityEngine.Component());
这样是被允许的。 - 类型参数多个时: 上述情况是在只有一个类型参数的情况,当拥有多个类型参数情况则不一样。
如void DoSomething<T,F>(T arg1,F arg2) ------>调用DoSomething("abc", 123) ;
这样依然可以正常调用
但是,如果方法的实参只使用了其中一个类型参数,类型推断依旧会失败。
如void DoSomething<T,F>(T arg1 ) ------>调用DoSomething("abc") ;
这样无法推断出类型,因为F没法明确。
由此可以看出,类型推断只不过是将调用泛型方法的显式指定类型变成依靠传参类型判断的隐式指定。
泛型类型并非是一种动态类型,只是抽象化为另一种新的类型,这种类型需要在调用时明确指定一个已有类型或隐式指定一个已有类型,没有强制转换或装箱操作。
这种类型推断对泛型类显然就不适用了。另外要明确,泛型方法并不需要依托于泛型类存在。
- 基本用法,格式为:XXX(){}
泛型原理
在.NET Framework 中,泛型在IL和CLR本身中具有本机支持。在编译泛型C#代码时,首先编译器会将其编译为IL,就像其他任何类型一样。但是,IL只包含实际特定类型的参数或占位符,并有专用的IL指令支持泛型操作。泛型代码的元数据中包含泛型信息。
真正的泛型实例化工作以“on-demand”的方式,发生在JIT编译时。当进行JIT编译时,JIT编译器用指定的类型实参来替换泛型IL代码元数据中的T,进行泛型类型的实例化。这会向JIT编译器提供类型特定的IL元数据定义,就好像从未涉及到泛型一样。这样,JIT编译器就可以确保方法参数的正确性,实施类型安全检查,甚至执行类型特定的IntelliSense。
当.NET将泛型IL代码编译为本机代码时,所产生的本机代码取决于指定的类型。如果本机指定的是值类型,则JIT编译器将泛型类型参数替换为特定的值类型,并且将其编译为本机代码。JIT编译器跟踪已经生成的类型特定的IL代码。如果JIT编译器用已经编译为本机代码的值类型编译泛型IL代码,则只是返回对该IL代码的引用。因为JIT编译器在以后的所有场合中都将使用相同的值类型特定的IL代码,所以不存在代码膨胀问题。
如果本机指定的是引用类型,则JIT编译器将泛型IL代码中的泛型参数替换为object,并将其编译为本机代码。在以后的任何针对引用类型而不是泛型类型参数的请求中,都将使用该代码。JIT编译器只会重新使用实际代码。实例仍然按照它们离开托管堆的大小分配空间,并且没有强制类型转换总结
因此,在需要使用同一套逻辑应用于多个类型时可以优先考虑使用泛型。
在数据结构中,尽量使用System.Collections.Generic;下的泛型结构来处理数据。
参考资料:
1. 《数据结构与算法》—段恩泽
2. 《Unity 3D脚本编程》—陈嘉栋