泛型中包含一个或多个类型参数(type parameters),类型参数以占位符的形式被声明,在运行时指定具体的类型。
类型参数的约束
where T : struct |
类型参数必须是不可为空值类型。 (所有的值类型都有一个公有的无参构造函数,所以此约束不能和new()约束一起使用) |
where T : class | 类型参数必须是一个引用类型。此约束可以应用于 class 、interface、delegate和array类型。(在C#8.0或更高版本中的可空上下文中,T必须是不可为空的引用类型。) |
where T : class? | 类型参数必须是引用类型,可以为Null或不可为Null都行。此约束可以应用于 class 、interface、delegate和array类型。 |
where T : notnull | 类型参数必须是不可为空的类型。(可以是引用类型也可以是值类型) |
where T : unmanaged | 类型参数必须是非托管类型,非托管约束隐含struct约束,不能与struct或new()约束组合。 |
where T : new() | 类型参数必须具有公共无参数构造函数。与其他约束一起使用时,必须最后指定new()约束。New()约束不能与struct和unmanaged约束组合。 |
where T : <base class name> |
类型参数必须是指定的基类或派生自指定基类的子类。 在 C# 8.0 及更高版本中的可为 null 上下文中,T 必须是从指定基类派生的不可为 null 的引用类型。 |
where T : <base class name>? |
类型参数必须是指定的基类或派生自指定基类的子类。 在 C# 8.0 及更高版本中的可为 null 上下文中,T 可以是从指定基类派生的可为 null 的引用类型。 |
where T : <interface name> |
类型参数必须是指定的接口或实现指定接口的类。可指定多个接口约束。 约束接口也可以是泛型。可空上下文中,T为非空类型。 |
where T : <interface name>? |
类型参数必须是指定的接口或实现指定接口的类。可指定多个接口约束。 约束接口也可以是泛型。可空上下文中,T为可空类型。 |
where T : U | 类型参数必须是U或U的子类。 |
约束指定了类型参数的功能。声明这些约束意味着可以使用约束类型内的操作和方法调用。如果泛型类或泛型方法中包含System.Object不支持的方法或属性,则必须对类型参数进行约束。约束就是为了告诉编译器,类型参数有哪些属性和方法。官网demo:
public class GenericList<T> { //泛型在嵌套类中同样有效 private class Node { // T用于非泛型的构造函数 public Node(T t) { next = null; data = t; } private Node next; public Node Next { get { return next; } set { next = value; } } //作为私有字段类型 private T data; // 作为公有属性类型 public T Data { get { return data; } set { data = value; } } } private Node head; public GenericList() { head = null; } // 作为方法的形参类型 public void AddHead(T t) { Node n = new Node(t); n.Next = head; head = n; } //作为返回值类型 public IEnumerator<T> GetEnumerator() { Node current = head; while (current != null) { yield return current.Data; current = current.Next; } } }
约束多个类型参数和多约束应用与一个类型参数:
class Base { } class Test<T, U> where U : struct where T : Base, new() { }
而对于无约束的类型参数,只能被当作System.Object去处理,而且不建议使用==和!=运算符,因为不能保证具体类型参数支持这些运算符。但是可以与null进行比较,如是类型参数是值类型,只会返回false。
协变和逆变
public class People{}//基类 public class Chinese : People{} //子类 static void Main(string[] args) { //错误代码,无法执行隐士转换 List<People> test = new List<Chinese>(); }
对于协变(使用out修饰泛型的类型参数)和逆变(使用in修饰泛型的类型参数),官方给出的定义大概是(我自己的理解+总结):协变是实际的返回类型比泛型参数定义的返回类型的派生程度更大,而逆变是传入的实参比泛型定义的参数的派生程度更小。
咋一看,有些懵逼,其实就是将类型参数的里氏替换原则应用到了外层,好像还是有些懵逼。举例说明吧:
IEnumerable<String> strings = new List<String>(); IEnumerable<Object> objects = strings;
List<T>和IEnumrable<T>具有继承关系,String和Objec也具有继承关系,但是IEnumerable<String>和IEnumerable<Object>没有继承关系而逆变和协变将这种继承关系扩展到了外层。
协变和逆变被统一称作“变体”,而且只能应用于接口或委托。变体中只支持引用类型,不支持值类型。
//编译失败,IEnumerable<int>不能隐式转换为IEnumerable<Object>,因为int是值类型 IEnumerable<int> integers = new List<int>();
变体解决了泛型的一个痛点问题:类型参数有继承关系,而泛型类型没有继承关系,泛型与泛型之间不能使用里氏替换:
//无法将List<string>隐士转换为 List<Object> List<Object> list = new List<string>();
协变泛型接口
可以使用 out
关键字将泛型类型参数声明为协变。 协变类型必须满足以下条件:
1、协变类型仅用作接口方法的返回类型,不能用作接口方法的参数类型。
interface ICovariant<out R> { //OK R GetSomething(); // 编译失败 //void SetSomething(R sampleArg); }
这里有一个例外,如果逆变类型的委托作为接口方法的参数是可以的。
interface ICovariant<out R> { void DoSomething(Action<R> callback); }
2、不能用作接口方法的泛型约束:
interface ICovariant<out R> { //编译失败,R必须是逆变或没有in 和 out 修饰的不变体 // void DoSomething<T>() where T : R; }
逆变泛型接口
可以使用 in
关键字将泛型类型参数声明为逆变。 逆变类型只能用作方法参数的类型,不能用作接口方法的返回类型。 逆变类型还可用于泛型约束。
interface IContravariant<in A> { void SetSomething(A sampleArg); void DoSomething<T>() where T : A; //编译失败 // A GetSomething(); }
此外,还可以在同一接口中同时支持协变和逆变,但需应用于不同的类型参数:
interface IVariant<out R, in A> { R GetSomething(); void SetSomething(A sampleArg); R GetSetSomethings(A sampleArg); }
变体泛型委托
public delegate T SampleGenericDelegate<T>(); public static void Test() { //可以为SampleGenericDelegate<String>和SampleGenericDelegate<Object>分配相同的lambda表达式,因为返回值类型存在隐式转换,方法的签名与委托类型相匹配。 SampleGenericDelegate<String> dString = () => " "; SampleGenericDelegate<Object> dObject = () => " "; //尽管String继承自Object,但是SampleGenericDelegate<String>不能隐式转换为SampleGenericDelegate<Object> // SampleGenericDelegate <Object> dObject = dString; }
与变体泛型接口相同,将委托中的泛型参数显式声明为协变或逆变,可以启用泛型委托之间的隐式转换。
public delegate O SampleGenericDelegate<in I, out O>(I i); public static void Test() { SampleGenericDelegate<Object, string> dString = (object str) => " "; SampleGenericDelegate<string, Object> dObject = dString; }
但是变体委托不能合并使用(多播委托)。因为多播委托的类型必须完全相同。否则在运行时抛出异常:
Action<object> actObj = x => Console.WriteLine("object: {0}", x); Action<string> actStr = x => Console.WriteLine("string: {0}", x); // System.ArgumentException: Delegates must be of the same type. // Action<string> actCombine = actStr + actObj; // actStr += actObj; // Delegate.Combine(actStr, actObj);