C#的泛型语法
泛型是OOP语言中三大特征的多态的最重要的体现,协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。除了协变和逆变,本文也涉及到C#泛型具体语法、原理、泛型约束、泛型缓存等内容。
泛型概述
C# 泛型语法 Since 2.0
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
根据不同的调用,生成不同的普通方法;泛型不是语法糖,是框架的升级所支持的;
泛型常见用法
普通子类继承泛型类/接口,那么必须指定泛型具体类型;
泛型子类继承泛型类/接口,那么必须指定泛型子类具体类型,泛型类/接口类型从泛型子类获取
/// <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);
泛型约束
常见的泛型约束有基类约束、接口约束、引用类型约束、值类型约束、无参构造函数约束、叠加约束
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
关键字:
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>(); // 逆变 + 协变
}
协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型。 逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。
类型安全的概念
先想想什么叫做类型安全?一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(如父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。
协变的意义
协变的话应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全。
逆变的意义
逆变就是将父类泛型隐式转换为子类泛型,其实逆变的内部也是实现子类转换为父类,所以说也是安全的。来看看为什么这么说,其实是观察视角不一样,前者是调用者视角,后者是实现者视角。
下面这个简单的例子也能说明问题:
public interface IListIn<in T>
{
void Show(T t);
}
public class ListIn<T> : IListIn<T>
{
public void Show(T t)
{
}
}
父类接口初始化子类实例时,父类对象无法调用子类方法所以T只能为返回值不能为参数称之为协变:
IListOut<Animal> List = new ListOut<Dog>();
子类接口初始化父类实例时,子类对象可以调用父类方法但返回对象未必为父类所以T只能为参数不能为返回值称之为逆变:
IListIn<Dog> List = new ListIn<Animal>();
泛型缓存
这个比较容易理解,JIT每次会把泛型对应的具体类型给实例化之后给缓存下来,不会重新创建新的类。
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());
}
}
}
}