(转).NET面试题系列[8]|(转).NET面试题系列[8] - 泛型

泛型相比反射,委托等较为抽象的概念要更接地气得多,而且在平常工作时,我们几乎时刻都和泛型有接触。大部分人对泛型都是比较熟悉的。
泛型集合是类型安全的集合。相对于泛型System.Collections.Generic,我们有类型不安全的集合System.Collections,其中的成员均为Object类型。一个经典的例子是ArrayList。
在使用ArrayList时,我们可以插入任意类型的数据,如果插入值类型的数据,其都会装箱为Object类型。这造成类型不安全,我们不知道取出的数据是不是想要的类型。泛型(集合)的数据类型是统一的,是类型安全的,没有装箱和拆箱问题,提供了更好的性能。为泛型变量设置默认值时常使用default关键字进行:T temp = default(T)。如果T为引用类型,则temp为null,如果T为值类型,则temp为0。
ArrayList的泛型集合版本为List。T称为类型参数。调用时指定的具体类型叫做实际参数(实参)。
面试必须知道的泛型三大好处:类型安全,增强性能,代码复用。
泛型集合的使用契机:几乎任何时候,都不考虑不用泛型集合代替泛型集合。很多非泛型集合也有了自己的泛型版本,例如栈,队列等。
泛型方法
泛型方法的使用契机一般为传入类型可能有很多种,但处理方式却相同的情境。这时我们可以不需要写很多个重载,而考虑用泛型方法达到代码复用的目的。配合泛型约束,可以写出更严谨的方法。泛型委托也可以看成是泛型方法的一种应用。
例如交换两个同类型变量的值:

static void Swap(ref T lhs, ref T rhs) { T temp; temp = lhs; lhs = rhs; rhs = temp; }

泛型约束
约束的作用是限制能指定成泛型实参(即T的具体类型)的数量。通过限制类型的数量,可以对这些类型执行更多的操作。例如下面的方法,T被约束为必须是实现了IComparable接口的类型。此时,传入的T除了拥有object类型的方法之外,还额外多了一个CompareTo方法。由于保证了传入的T必须是实现了IComparable接口的类型,就可以肯定T类型一定含有CompareTo方法。如果去掉约束,o1是没有CompareTo方法的。
static int Compare(T o1, T o2) where T : IComparable { return o1.CompareTo(o2); }

此时如果将object类型的数据传入方法,则会报错。因为object没有实现IComparable接口。
泛型约束分为如下几类:
  • 接口约束:泛型实参必须实现某个接口。接口约束可以有多个。
  • 基类型约束:泛型实参必须是某个基类的派生类。特别的,可以指定T : class / T : struct,此时T分别只能为引用类型或值类型。基类型约束必须放在其他约束之前。
  • 构造函数new()约束:泛型实参必须具有可访问的无参数构造函数(默认的也可)。new()约束出现在where子句的最后。
如果泛型方法没有任何约束,则传入的对象会被视为object。它们的功能比较有限。不能使用 != 和 == 运算符,因为无法保证具体类型参数能支持这些运算符。
协变和逆变
可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。其对应的术语则是不变性(invariant)。
可变性
可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。例如对普通继承中的可变性:若某方法声明返回类型为Stream,在实现时可以返回一个MemoryStream。可变性有两种类型:协变和逆变。
协变性:可以建立一个较为一般类型的变量,然后为其赋值,值是一个较为特殊类型的变量。例如:
string str = "test"; // An object of a more derived type is assigned to an object of a less derived type. object obj = str;

因为string肯定是一个object,所以这样的变化非常正常。
逆变性:在上面的例子中,我们无法将str和一个新的object对象画等号。如果强行要实现的话,只能这么干:
string s = (string) new object();

但这样还是会在运行时出错。这也告诉我们,逆变性是很不正常的。
泛型的协变与逆变
协变性和out关键字搭配使用,用于向调用者返回某项操作的值。例如下面的接口仅有一个方法,就是生产一个T类型的实例。那么我们可以传入一个特定类型。如我们可以将IFactory视为IFactory。这也适用于Food的所有子类型。(即将其视为一个更一般类型的实现)
interface IFactory { T CreateInstance(); }

逆变性则相反,和in关键字搭配使用,指的是API将会消费值,而不是生产值。此时一般类型出现在参数中:
interface IPrint { void Print(T value); }

这意味着如果我们实现了IPrint,我们就可以将其当做IPrint使用。(即将其视为一个更具体类型的实现)
如果存在双向的传递,则什么也不会发生。这种类型是不变体(invariant)。
interface IStorage { byte[] Serialize(T value); T Deserialize(byte[] data); }

这个接口是不变体。我们不能将它视为一个更具体或更一般类型的实现。
假设有如下继承关系People –> Teacher,People –> Student。
如果我们以协变的方式使用(假设你建立了一个IStorage< Teacher >的实例,并将其视为IStorage)则我们可能会在调用Serialize时产生异常,因为Serialize方法不支持协变(如果参数是People的其他子类,例如Student,则IStorage< Teacher >将无法序列化Student)。
如果我们以逆变的方式使用(假设你建立了一个IStorage的实例,并将其视为IStorage< Teacher >),则我们可能会在调用Deserialize时产生异常,因为Deserialize方法不支持逆变,它只能返回People不能返回Teacher。
使用in和out表示可变性
如果类型参数用于输出,就使用out,如果用于输入,就使用in。注意,协变和逆变性体现在泛型类T和T的派生类。目前out 和in 关键字只能在接口和委托中使用。
IEnumerable支持协变性
IEnumerable支持协变性,它允许一个类似下面签名
void 方法(IEnumerable anIEnumberable)

的方法,该方法传入更具体的类型(T的派生类),但在方法内部,类型会被看成IEnumerable。注意out关键字。
下面的例子演示了协变性。我们利用IEnumerable的协变性,传入较为具体的类型Circle。编译器会将其看成较为抽象的类型Shape。
public class Program { public static void Main(string[] args) { var circles = new List { new Circle(new Point(0, 0), 15), new Circle(new Point(10, 5), 20), }; var list = new List(); //泛型的协变: //AddRange传入的是特殊的类型List,但要求是一般的类型List //AddRange方法签名:void AddRange(IEnumerable collection) //IEnumerable允许协变(对于LINQ来说,协变尤其重要,因为很多API都表示为IEnumerable) list.AddRange(circles); //C# 4.0之前只能这么做 list.AddRange(circles.Cast()); } }public sealed class Circle : IShape { private readonly Point center; public Point Center { get { return center; } }private readonly double radius; public double Radius { get { return radius; } }public Circle(Point center, int radius) { this.center = center; this.radius = radius; }public double Area { get { return Math.PI * radius * radius; } } }public interface IShape { double Area { get; } }

IComparer支持逆变性
IComparer支持逆变性。我们可以简单的实现一个可以比较任何图形面积的方法,传入的输入类型(in)是最General的类型IShape。之后,在使用时,我们获得的结果是较为具体的类型Circle。因为任何图形都可以比较面积,圆形当然也可以。
注意IComparer的签名是public interface IComparer
public class Program { public static void Main(string[] args) { var circles = new List { new Circle(new Point(0, 0), 15), new Circle(new Point(10, 5), 20), }; //泛型的逆变: //AreaComparer可以比较任意图形的面积,但我们可以传入具体的图形例如圆或正方形 //Compare方法签名:Compare(IShape x, IShape y) //IComparer支持逆变 //传入的是圆形Circle,但要求的输入是IShape circles.Sort(new AreaComparer()); } }class AreaComparer : IComparer { public int Compare(IShape x, IShape y) { return x.Area.CompareTo(y.Area); } }

C#中泛型可变性的限制
  1. 不支持类的类型参数的可变性。只有接口和委托可以拥有可变的类型参数。in 和 out 修饰符只能用来修饰泛型接口和泛型委托。
  2. 可变性只支持引用转换。可变性只能用于引用类型,禁止任何值类型和用户定义的转换,如下面的转换是无效的: