编辑
2022-06-13
编程语言
00
请注意,本文编写于 468 天前,最后修改于 105 天前,其中某些信息可能已经过时。

目录

泛型概述
泛型原理
泛型常见用法
泛型约束
协变和逆变
类型安全的概念
协变的意义
逆变的意义
泛型缓存

泛型是OOP语言中三大特征的多态的最重要的体现,协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。除了协变和逆变,本文也涉及到C#泛型具体语法、原理、泛型约束、泛型缓存等内容。

泛型概述

C# 泛型语法 Since 2.0

c#
using System; using System.Collections.Generic; namespace AdvanceCS { public class Program { public static void Main(string[] args) { Console.WriteLine("Hello World!"); ShowObject(10); ShowObject("zouchanglin"); object oVal = 1024.0f; ShowObject(oVal); Console.WriteLine("==============="); Show<int>(20); Show("zouchanglin"); // 尖括号类型可省略 Show(oVal); Console.WriteLine("==============="); Console.WriteLine(typeof(List<>)); Console.WriteLine(typeof(Dictionary<,>)); } // 延迟声明类型:把参数类型的声明推迟到调用 private static void Show<T>(T param) { Console.WriteLine("This is {0}, param = {1}, type = {2}", nameof(Program), param, param.GetType().Name); } // 早期泛型替代用法 private static void ShowObject(object obj) { Console.WriteLine("This is {0}, param = {1}, type = {2}", nameof(Program), obj, obj.GetType().Name); } } }

ShowObject这种传递object类型的方式如果是传的值类型会有拆装箱的性能损失,已经不推荐使用了

泛型原理

泛型需要编译器和JIT支持,原来定义的时候就是用了个T作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符, CLR / JIT根据不同的调用,生成不同的普通方法;泛型不是语法糖,是框架的升级所支持的;

泛型常见用法

普通子类继承泛型类/接口,那么必须指定泛型具体类型;

泛型子类继承泛型类/接口,那么必须指定泛型子类具体类型,泛型类/接口类型从泛型子类获取

c#
/// <summary> /// 普通类继承泛型抽象类、泛型接口 /// </summary> /// <typeparam name="T"></typeparam> public class GenericClass<T, T2> : GenericAbsClass<T>, IGenericInterface<T2> { private T _t; public GenericClass(T t) { this._t = t; } public override T GetT() { throw new NotImplementedException(); } public void SetT(T2 t) { throw new NotImplementedException(); } } /// <summary> /// 泛型接口 /// </summary> /// <typeparam name="T"></typeparam> public interface IGenericInterface<T> { void SetT(T t); } /// <summary> /// 泛型抽象类 /// </summary> /// <typeparam name="T"></typeparam> public abstract class GenericAbsClass<T> { public abstract T GetT(); } /// <summary> /// 泛型委托 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> public delegate void demoDelegate<T>(T t);

泛型约束

常见的泛型约束有基类约束、接口约束、引用类型约束、值类型约束、无参构造函数约束、叠加约束

c#
public class Constraint { /// <summary> /// 泛型基类约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show1<T>(T param) where T: Person // 泛型基类约束,类似于Java <? extends Type> // 泛型基类约束不能是密封类(sealed) { param.PrintName(); } /// <summary> /// 泛型接口约束,和基类约束一样 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show2<T>(T param) where T: Work // 泛型接口约束,和基类约束一样 { param.StartWork(); } /// <summary> /// 引用类型约束 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show3<T>(T param) where T : class // 引用类型约束 { param = null; } /// <summary> /// 值类型约束,值类型可用default赋予默认值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show4<T>(T param) where T : struct // 值类型约束 { param = default(T); // 根据T的不同赋予默认值 } /// <summary> /// 无参构造函数约束, T类型必须有无参构造 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show5<T>(T param) where T : new() // 无参构造函数约束 { T TNew = new T(); } /// <summary> /// 约束可以叠加,比较灵活 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="param"></param> public void Show6<T>(T param) where T : Person,Work,new() // 基类约束 + 接口约束 + 无参构造参数约束 { param.PrintName(); param.StartWork(); } public static void Main() { } }

协变和逆变

上面的报错也就是对于编译器来说,虽然Person和Student是父子关系,但是List<Person>List<Student>就不是父子关系,但是正常逻辑来讲的话List<Person>List<Student>是存在父子关系的。

协变就是为了解决这一问题的,这样做其实也是为了解决类型安全问题。

因为协变只能用在接口或者委托类型中,所以我们将List抽象抽来一个空接口 IMyList,然后实现该接口,协变是在T泛型前使用 out 关键字,逆变是在T泛型前使用 in 关键字:

c#
public interface IMyList<in TIn, out TOut> { // 逆变用与参数输入 void Add(TIn t); // 协变用于返回值输出 TOut Get(); } public class MyList<TIn, TOut> : IMyList<TIn, TOut> { public void Add(TIn t) { Console.WriteLine("Add"); } public TOut Get() { Console.WriteLine("Get"); return default(TOut); } } public class Person { public string Name { set; get; } public void PrintName() { Console.WriteLine("name = " + this.Name); } } public class Student : Person { public string StuId { set; get; } } public static void Main() { List<Person> pList1 = new List<Person>(); //List<Person> sList = new List<Student>(); // 逻辑上是没错的,但是编译器报错 //List<Person> sList = new List<Student>().Select(c => (Person)c).ToList(); IMyList<Student, Person> list1 = new MyList<Student, Person>(); IMyList<Student, Person> list2 = new MyList<Student, Student>(); // 协变 IMyList<Student, Person> list3 = new MyList<Person, Person>(); // 逆变 IMyList<Student, Person> list4 = new MyList<Person, Student>(); // 逆变 + 协变 }

协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。 逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。

类型安全的概念

先想想什么叫做类型安全?一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(如父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。

协变的意义

协变的话应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全。

逆变的意义

逆变就是将父类泛型隐式转换为子类泛型,其实逆变的内部也是实现子类转换为父类,所以说也是安全的。来看看为什么这么说,其实是观察视角不一样,前者是调用者视角,后者是实现者视角。

下面这个简单的例子也能说明问题:

c#
public interface IListIn<in T> { void Show(T t); } public class ListIn<T> : IListIn<T> { public void Show(T t) { } }

父类接口初始化子类实例时,父类对象无法调用子类方法所以T只能为返回值不能为参数称之为协变:

c#
IListOut<Animal> List = new ListOut<Dog>();

子类接口初始化父类实例时,子类对象可以调用父类方法但返回对象未必为父类所以T只能为参数不能为返回值称之为逆变:

c#
IListIn<Dog> List = new ListIn<Animal>();

泛型缓存

这个比较容易理解,JIT每次会把泛型对应的具体类型给实例化之后给缓存下来,不会重新创建新的类。

c#
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AdvanceCS { /// <summary> /// 每个不同的T,都会生成一份不同的副本 /// 适合不同类型,需要缓存一份数据的场景,效率高 /// </summary> /// <typeparam name="T"></typeparam> class GenericCache<T> { static GenericCache() { Console.WriteLine("This is GenericCache static Constructor."); _TypeTime = $"{typeof(T)}_{DateTime.Now.ToString()}"; } private static string _TypeTime = ""; public static string GetCache() { return _TypeTime; } } public class Test { public static void Main() { for (int i = 0; i < 5; i++) { Console.WriteLine(GenericCache<int>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<string>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<double>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<float>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<DateTime>.GetCache()); Thread.Sleep(10); Console.WriteLine(GenericCache<Test>.GetCache()); } } } }

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!