文档资料

泛型的作用

  • 跨类型可复用的代码:继承和泛型。
    • 继承 -> 基类。
    • 泛型 -> 带有“(类型)占位符”的“模板”。

泛型类型

  • 泛型会声明类型参数 - 泛型的消费者需要提供类型参数(arguments)来把占位符类型填充上。
public class Stack<T>
{
	int position;
	T[] data = new T[100];
	public void Push(T obj) => data[position++] = obj;
	public T Pop() => data[--position];
}

var stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
int x = stack.Pop();	// x is 10
int y = stack.Pop();	// y is 5
  • Stack<T>: Open Type 开放类型。
  • Stack<int>: Closed Type 封闭类型。
  • 在运行时,所有的泛型类型实例都是封闭的(占位符类型已被填充了)
public class ###
{
	int position;
	int[] data = new int[100];
	public void Push(int obj) => data[position++] == obj;
	public int Pop() => data[--position];
}
var stack = new Stack<T>(); // 不合法,T 要指明具体类型。

public class Stack<T>
{
	public Stack<T> Clone()
	{
		Stack<T> clone = new Stack<T>();	// 合法,在泛型类型里面可以使用 T 来代表类型。
	}
}

为什么泛型会出现

不使用泛型实现 Stack

public class ObjectStack
{
	int position;
	object[] data = new object[10];
	public void Push(object obj) => data[position++] = obj;
	public object Pop() => data[--position];
}
  • 需要装箱和向下转换,这种转换在编译时无法进行检查。
// We want int
stack.Push("s");	// Wrong type, but no error!
int i = (int) stack.Pop();	// Downcast - runtime error

泛型方法

  • 泛型方法在方法的签名内也可以声明类型参数。
static void Swap<T>(ref T a, ref T b)
{
	T temp = a;
	a = b;
	b = temp;
}

int x = 5;
int y = 10;
Swap(ref x, ref y);
  • 在泛型类型里面的方法,除非也引入(使用了<T>)了类型参数(type parameters),否则是不会归为泛型方法的。
public class Stack<T>
{
	int position;
	T[] data = new T[100];
	public void Push(T obj) => data[position++] = obj;
	
	// Pop() 不是泛型方法,因为类型参数没有 T,只是一个泛型类型里面的普通方法。
	public T Pop() => data[--position];
}
  • 只有类型和方法可以引入(使用了<T>)类型参数,属性、索引器、事件、字段、构造函数、操作符等都不可以声明类型参数,但是他们可以使用他们所在的泛型类型的类型参数。
public T this[int index] => data[index];	// 合法,没有 <T>

public Stack<T>() {}	// 不合法,构造函数不能引入类型参数

声明类型参数

  • 在声明 classstructinterfacedelegate 的时候可以引入(使用了<T>)类型参数(Type parameters)。
  • 其他的例如属性,就不可以引入(使用了<T>)类型参数,但是可以使用类型参数。
public struct Nullable<T>
{
	public T Value { get; }
}
  • 泛型类型和泛型方法可以有多个类型参数。
class Dictionary<TKey, TValue> {...}

var myDic = new Dictionary<int, string>();
  • 泛型类型和泛型方法的名称可以被重载,条件是参数类型的个数不同。
class A {}
class A<T> {}
class A<T1, T2> {}
  • 按约定,泛型类型和花园泛型方法,如果只有一个参数类型,那么就叫 T
  • 当使用多个类型参数的时候,每个类型参数都使用 T 作为前缀,随后跟着具有描述性的一个名字,例如 <Tkey, TValue>

typeof 与未绑定的泛型类型

  • 开放的泛型类型在编译后就变成了封闭的泛型类型。
  • 但是如果作为 Type 对象,那么未绑定的泛型类型在运行时是可以存在的。(只能通过 typeof 操作符来实现)
class A<T> {}
class A<T1, T2> {}
...
Type a1 = typeof(A<>);	// Unbound type (notice no T)
Type a2 = typeof(A<,>);	// Use commas to indicate multiple type args.

泛型的默认值

  • 使用 default 关键字来获取泛型类型参数的默认值。
static void Zap<T>(T[] array)
{
	for (int i = 0; i < array.Length; i++)
		array[i] = default(T);	// 引用类型默认值为 null
}

泛型的约束

  • 默认情况下,泛型的类型参数(parameter)可以是任何类型的。
  • 如果只允许使用特定的类型参数(argument),就可以指定约束。
where T : base-class	// T 必须是某个父类的子类
where T : interface	// T 必须实现 interface 接口
where T : class	// T 必须是引用类型
where T : struct	// T 必须是值类型,但不可以是可空值类型
where T : new()	// T 必须有一个无参构造函数
where U : T	// U 必须继承于 T
  • 泛型的约束可以作用于类型或方法的定义。
public interface IComparable<T>
{
	int CompareTo(T other);
}

static T Max<T>(T a, T b) where T : IComparable<T>
{
	return a.CompareTo(b) > 0 ? a : b;
}

int z = Max(5, 10);	// 10
string last = Max("ant", "zoo");	// zoo

泛型类型的子类

  • 泛型类可以有子类,在子类里,可以继续让父类的类型参数保持开放。
class Stack<T> {}
class SpecialStack<T> : Stack<T> {}
  • 在子类里,也可以使用具体的类型来封闭父类的类型参数。
class IntStack : Stack<int> {}
  • 子类型也可以引入新的类型参数。
class List<T> {}
class KeyedList<T, TKey> : List<T> {}
  • 技术上来讲,所有子类的类型参数都是新鲜的。你可以认为子类先把父类的类型参数(argument)给关闭了,然后又打开了。为这个先关闭后打开的参数(argument)带来新的名称或含义。
class List<T> {...}
class KeyedList<TElement, Tkey> : List<TElement> {...}

自引用的泛型声明

  • 在封闭类型参数(argument)的时候,该类型可以把它自己作为具体的类型。
public interface IEquatable<T> { bool Equals(T obj); }

public class Balloon : IEquatable<Balloon>
{
	public string Color { get; set; }
	public int CC { get; set; }
	
	public bool Equals(Balloon b)
	{
		if (b == null) return false;
		return b.Color == Color && b.CC == CC;
	}
}

class Foo<T> where T : IComparable<T> {...}
class Bar<T> where T : Bar<T> {...}

静态数据

  • 针对每一个封闭类型,静态数据是唯一的。
class Boo<T> { public static int Count; }
class Test
{
	static void Main()
	{
		Console.WriteLine(++Bob<int>.Count);	// 1
		Console.WriteLine(++Bob<int>.Count);	// 2
		Console.WriteLine(++Bob<string>.Count);	// 1
		Console.WriteLine(++Bob<object>.Count);	// 1

	}
}

类型参数和转换

  • C# 的转换操作符支持下列转换:
    • 数值转换
    • 引用转换
    • 装箱拆箱转换
    • 自定义转换
  • 决定采用的是哪种转换,发生在编译时,根据已知类型的操作数来决定。
  • 但是泛型在编译时不知道具体的类型,所以泛型转换会编译失败。
StringBuilder Foo<T>(T arg)
{
	if (arg is StringBuilder)
		return (StringBuilder) arg;	// Compile-time error
}
  • 使用 as 操作符实现泛型转换。
StringBuilder Foo<T>(T arg)
{
	StringBuilder sb = arg as StringBuilder;
	if (sb != null) return sb;
}
  • 或者使用装箱拆箱转换。
StringBuilder Foo<T>(T arg)
{
	if (arg is StringBuilder)
		return (StringBuilder) (object) arg;
}

int Foo<T>(T x) => (int) x;	// Compile-time error
int Foo<T>(T x) => (int) (object) x;

协变和逆变

  • 通过的代码:
IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings;
  • 报错的代码:
IList<string> strings = new List<string> { "a", "b", "c" };
IList<object> objects = strings;
  • 因为 IList 中的一些方法操作 object 的时候是危险的。假如报错的代码成立,那么
objects.Add(new objects());
string element = strings[3];	// strings[3] 返回的是 object 而不是 string
  • 具体错误:在 .Net 源码中,IEnumerable<T> 中的 T 只做返回类型使用。而 IList<T> 中的 T 既做返回又做输入参数。
  • 还有一种 T 只做输入参数的泛型类型。下面的代码是成立的。
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction;
stringAction("Print me");

Covariance, Contravariance, Invariance

  • Covariance 协变:当值作为返回值或 out 输出
    • IEnumerable<string> -> IEnumerable<object>
    • public interface IEnumerable<out T>
    • 子类的 T 可以转换为父类的 T
    • <out T> 就是协变。
  • Contravariance 逆变:当值作为参数输入。
    • Action<object> -> Action<string>
    • public delegate void Action<in T>
    • 父类的 T 可以转换为子类的 T
    • <in T> 就是逆变。
  • Invariance 不变:当值既是输入又是输出。
    • public interface IList<T>
    • 既不能向上,也不能向下转换。
    • 没有 out/in 修饰符的就是不变。

Variance

  • 前面的协变、逆变、不变都称为 variance。
  • Variance 只能出现在接口和委托里。
  • Variance 转换时引用转换的一个例子。引用转换就是指,你无法改变其底层的值,只能改变实例的编译时的类型。
  • Identity conversion 本体转换:对 CLR 而言从一个类型转换到相同的类型。例如 object -> dynamic

合理的转换

  • 如果从 A 到 B 的转换是本体转换或者隐式引用转换,那么从 IEnumerable<A> -> IEnumerable<B> 的转换就是合理的。
    • IEnumerable<string> -> IEnumerable<object>
    • IEnumerable<string> -> IEnumerable<IConvertible>
    • IEnumerable<IDsposable> -> IEnumerable<object>

不合理的转换

  • IEnumerable<object> -> IEnumerable<string>
  • IEnumerable<string> -> IEnumerable<Stream>
  • IEnumerable<int> -> IEnumerable<IConvertible>
    • 虽然是隐式转换,但这是装箱操作而不是引用转换。
  • IEnumerable<int> -> IEnumerable<long>
    • 两个值类型,不是引用转换。