8.泛型 泛型约束
泛型:作用是“代码重用”,用于“算法重用”。CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型,还可以创建泛型接口和泛型委托。
原则:泛型参数变量为T,或者以T开头(如TKey TValue),类似于接口I的原则;
8.1 泛型
泛型最常见的应用是集合类。
8.2 泛型约束
参考:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
8.2.1 泛型约束种类
在定义泛型类时,可以对客户端代码实例化类时所使用的类型参数添加一些约束;如果客户端代码尝试使用约束所不允许的类型来实例化类,则会产生编译错误。通过使用where关键字指定约束,下面为六种类型的约束:
a. where T: struct:类型参数必须是值类型。可以指定除nullable外的任何值类型。
b. where T: class:类型参数必须是引用类型;这同样适用于所有类、接口、委托或数组类型;
c. where T: new():类型参数必须具有public无参的构造函数;当与其他约束一起使用时,new()必须放到最后;
d. where T: <基类名>:类型参数必须是约束中的基类,或基类的派生类型;
e. where T: <接口名称>:类型参数必须是指定接口,或实现指定接口的类型,可指定多个接口约束,约束接口也可以是泛型;
f. where T: <接口名称>:msdn没理解;
注意:
1. 如果同时使用struct 和 new() 约束,则编译器会报错,因为值类型都隐式的有一个无参数的构造器,C#认为这样的约束是多余的;
2. 所有的值类型都隐式地有一个公共无参构造器;但是有的引用类型没有公共无参构造器;(例如定义一个用于单例的类,构造器可能是私有的)
8.2.2 使用泛型约束原因
例如在一个泛型类中,要检查泛型列表中的某个项是否有效,或者将它与其他项比较,那么编译器必须保证client代码指定的参数支持它需要调用运算符或者方法,因此就需要添加泛型约束。编译器有了此保证后,就能够允许在泛型类中调用该类型的方法。
例如下面的例子:
public class Employee
{
private string name;
private int id;
public Employee(string s, int i)
{
name = s;
id = i;
}
public string Name
{
get { return name; }
set { name = value; }
}
public int ID
{
get { return id; }
set { id = value; }
}
}
public class GenericList<T> where T : Employee
{
private class Node
{
private Node next;
private T data;
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
public GenericList() //constructor
{
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;
}
}
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法的数量。
注意:
在使用where T:class约束时,需要避免对类型参数使用 == !=运算符,因为==只测试引用是否相同,但不测试值是否相同(==与Equals的区别),即使在类型参数中重载了这些运算符。
如果必须测试值的相等性,使用where T : IComparable<T>
约束,并在使用构造泛型类的类中实现该接口。
8.2.3 约束多个参数
可以对多个参数应用多个约束,对一个参数应用多个约束,例如:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new() { }
8.2.4 其他需要注意问题
a. 泛型类型变量的转型:
将泛型类型的变量转型为其他类型是非法的,除非转型为约束兼容的类型;例如:
private static void CastingAGenericTypeVariable<T>(T obj)
{
int x = (int) obj; //报错
int y = (int) (object) obj;//不报错
}
第二行不会报错,但是运行时可能抛出InvalidCastException异常;
b. 将泛型类型变量设置为默认值
例如:将泛型T设置为null会报错,因为T可能是值类型,需要加上 where T:class的约束,为引用类型即可;
private static void SettingAGenericTypeVariableToNull<T>()
{
T temp = null;
}
可用default关键字设置默认值,例如
private static void SettingAGenericTypeVariableToNull<T>()
{
T temp = default(T);
}
上述代码中,default告诉C#编译器和CLR的JIT编译器,如果T是引用类型,则将temp设置为null;如果为值类型,则temp的所有位都设置为0;
c. 将泛型类型变量与null进行比较
private static void ComparingAGenericTypeVariableWithNull<T>(T obj)
{
if (obj == null) //对于值类型,永远不会执行
{
}
}
上述代码,C#编译器不会报错
d. 编译器在编译时确定不了类型,所以不能向泛型类型的变量应用任何操作符。
8.3 委托和结构的逆变和协变泛型类型实参
convariant: 协变量 -- 指定返回类型的兼容性;
contravariant: 逆变量 -- 指定参数的兼容性;
泛型中的协变、逆变:https://segmentfault.com/a/1190000007005115