.NET开发技术栈: 泛型
什么是泛型
C#是一门强类型的静态语言,所有变量的数据类型都是在编译时确定的。我们可以在编写类或方法等元素时先不确定类型,而是用类型占位符T(T可以为任意其他名字,比如K, Ts,TP)代替, 再真正调用时再把类型传进来。这样能使我们构建的类, 方法等元素更具通用型,
避免为每个数据类型写一个特定的方法。
泛型的基本使用方法
假设有一个方法,接收一个参数, 然后在控制台打印出来,并返回传入的参数, 代码如下:
public string PrintMsg(string msg) { Console.WriteLine(msg);
return msg; }
此处参数的类型为string, 如果我们要讲参数类改为 int , DateTime, decimal时,我们需要重新定义三个方法
public int PrintMsg(int msg) { Console.WriteLine(msg); return msg; } public DateTime PrintMsg(DateTime msg) { Console.WriteLine(msg); return msg; } public decimal PrintMsg(decimal msg) { Console.WriteLine(msg); return msg; }
这些方法的内部实现逻辑完全一样, 有所不同的只是参数和返回值的数据类型, 这时候泛型就可以上场了,改为泛型实现如下
public T PrintMsg<T>(T msg) { Console.WriteLine(msg); return msg; }
调用时,我们只需把想要的类型传进去就可以了
public void Test() { string msg = "泛型如此简单"; PrintMsg<string>(msg); }
程序执行时,方法内的T将被确定下来, 此处为string, 就相当于执行了这个代码
public string PrintMsg(string msg) { Console.WriteLine(msg); return msg; }
若传入int, DateTime, decimal ,其他类型,包括自定义类, 也是一样高的效果。
.NET中的泛型原理
泛型的类型是在什么时候确定下来的呢, 这里就得提一下.NET的运行机制了,我们编写完的c#代码经过编译, 会生成exe或dll文件, 这些文件中包含的是中间语言IL, 在运行程序时,IL会被JIT编译, 生成cpu真正认识的二进制机器码,大致如下图

泛型中的类型就是在JIT编译的这一步被确定下来的,真正运行时JIT会根据传入的类型参数,生成一个此类型参数的普通类或方法。
因此泛型方法和普通方法在执行效率上几乎是一样的。
也因为这一特点,产生了泛型缓存这一概念。
泛型缓存
class Cache<T> { private static T cache_value; public static object Get() { return cache_value; } public static void Set(T value) { cache_value = value; } }
以上就是一个最简单的泛型缓存类,传入不同的类型参数,在运行时会生成与之对应的不同副本,也就是说,每个类型一个缓存类,这样就能很好地缓存不同类型的值。
泛型约束
Cat, Dog 两个类继承自Animal类,代码如下
public abstract class Animal { public string name { get; set; } public abstract void sleep(); } public class Cat : Animal { public override void sleep() { Console.WriteLine($"{name} sleep in day"); } } public class Dog : Animal { public override void sleep() { Console.WriteLine($"{name} sleep in night"); } }
PetShop是一个泛型类, where T:Animal的意思是: 传入的类型参数必须继承自Animal类
public class PetShop<T> where T : Animal { public void ShowSleepTime (T animal) { animal.sleep(); } }
使用泛型约束后,可以访问到约束类型的方法或字段, 比如上面代码中的animal.sleep() 访问的就是类Animal中的方法。
泛型约束一共有以下六种类
-
- where T : struct 结构约束, 类型T必须是值类型
- where T : class 类约束, 类型T必须是引用类型
- where T : new() 构造函数约束,类型T必须有一个默认的构造函数
- where T : IFoo 接口约束, 类型T必须实现接口IFoo
- where T : Foo 类约束, 类型T必须继承基类Foo
- where T1 : T2 类型T1必须继承泛型T2
泛型的种类
- 泛型类
- 泛型方法
- 泛型接口
- 泛型委托
- 泛型集合
逆变协变
Animal cat = new Cat();
一只猫是一只动物,非常正确!
List<Animal> cats = new List<Cat>();
一组猫是一组动物, 按理说也是非常正确, 但此处编译器会报错: 无法将类型List<Cat> 转换为 List<Animal>。
究其原因,是因为List<Cat> 和 List<Animal> 这两个类并没有继承关系,这么写当然是错误的。
我们自己定义个接口, 然后写一个泛型类继承此接口 (这里只是用于演示泛型的协变逆变,未实现通常List所拥有的方法)
public interface IMyList<T> { T Get(); void Show(T t); } public class MyList<T> : IMyList<T> { public T Get() { return default(T); } public void Show(T t) { //show object t } }
现在我们是否可以这么写
IMyList<Animal> cats = new MyList<Cat>();
MyLsit继承自IMyList, Cat继承自Animal, 看上去似乎已经天衣无缝, 但是编译器还是报错。
解决问题
协变:
IMyList的类型参数加out关键字
public interface IMyList<out T> { T Get(); // void Show(T t); }
加上out关键字后
IMyList<Animal> cats = new MyList<Cat>();
这个代码不报错了, 但是这里的 T 只能作返回值, 不能作参数,
逆变:
IMyList的类型参数加in关键字
public interface IMyList<in T>
{
// T Get();
void Show(T t);
}
加上in关键字后
IMyList<Cat> cats = new MyList<Animal>();
这个代码是不会报错的,编译通过。
为什么协变out后类型参数不能作为参数传入, 逆变in后参数类型不能作为返回值返回, 可以结合代码,自行思考其逻辑可行性。
协变逆变小结:
协变和逆变只有泛型接口和泛型委托有 ,用out和in关键字约束类型参数是作为返回值还是参数。
协变用out修饰,类型参数只能作返回值, 可以实现 IMyList<Animal> animal = new MyList<Cat>() 这种用子类替换父类的情况。
逆变用in修饰,类型参数只能作参数, 可以实现 IMyList<Cat> animal = new MyList<Animal>() 这种用父类替换子类的情况。
浙公网安备 33010602011771号