泛型
csharp 有两种不同的机制来编写跨类型复用的代码:继承和泛型。但继承的复用性来自基类,而泛型的复用性是通过带有“占位符”的“模板”类型实现的。和继承相比,泛型能够提供类型的安全性,并减少类型的转换和装箱。
1 泛型类型 Generic Types
泛型类型中声明的类型参数(占位符类型)需要有泛型类型的消费者(即提供类型参数的一方)填充。下面是一个存放类型 T
实例的泛型栈类型 Stack<T>
。Stack<T>
声明了单个类型参数 T
:
public class Stack<T>
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
使用 Stack<T>
的方式如下:
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>
用参数类型 int
填充 T
,这会在运行时隐式创建一个类型:Stack<int>
。若试图将一个字符串加入 Stack<int>
中则会产生一个编译时错误。Stack<int>
具有如下定义:
public class 类名
{
int position;
int[] data = new int[100];
public void Push (int obj) => data[position++] = obj;
public int Pop() => data[--position];
}
-
Stack<T>
开放类型 Open Type -
Stack<int>
封闭类型 Closed Type -
在运行时,所有的泛型实例都是封闭的,占位符已经被类型填充,意味着:
var stack = new Stack<T>(); // Illegal: What is T?
-
只有在类或者方法的内部,
T
才可以定义为类型参数:public class Stack<T> { ... public Stack<T> Clone() { Stack<T> clone = new Stack<T>(); // Legal ... } }
2 为什么需要泛型
泛型是为了代码能够跨类型复用而设计的。假定我们需要一个整数栈,如果不使用泛型:
-
方案一:为每一个需要的元素类型硬编码不同版本的类如
IntStack
、StringStack
等导致大量的代码重复。 -
方案二:用
object
作为元素类型的栈:public class ObjectStack { int position; object[] data = new object[10]; public void Push (object obj) => data[position++] = obj; public object Pop() => data[--position]; }
但
ObjectStack
不会像硬编码的IntStack
类一样只处理整数元素。而且ObjectStack
需要用到装箱和向下类型转换,而这些都不能在编译时进行检查:// Suppose we just want to store integers here: ObjectStack stack = new ObjectStack(); stack.Push ("s"); // Wrong type, but no error! 编译时不会报错,但在运行时会报错 int i = (int)stack.Pop(); // Downcast - runtime error
🐴 吧啦吧啦吧啦,因此我们需要泛型。🐴
🌾 ObjectStack
在功能上等价于 Stack<object>
。
3 泛型方法
泛型方法在方法的签名中声明类型参数。
static void Swap<T> (ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
Swap<T>
的使用方法如下:
int x = 5;
int y = 10;
Swap(ref x, ref y);
通常调用泛型方法不需要提供类型参数,因为编译器可以隐式推断得出。如果有二义性,则使用如下方法:
Swap<int> (ref x, ref y);
🌾 声明泛型方法时,可以在返回值类型中使用这个类型参数,此时,编译器的类型推断功能不适用于仅在返回值类型中使用类型参数的情况,此种情况在调用时必须显式指定类型参数:
T MyFunc<T>()
{
return default(T);
}
-
在泛型中,只有引入类型参数(用尖括号标出)的方法才可以归为泛型方法。在泛型
Stack
类中的Pop
方法仅仅使用了类型中已有的参数T
,因此不属于泛型方法。 -
唯有方法和类可以引入类型参数。属性、索引器、事件、字段、构造函数、运算符等都不能声明类型参数,只能参与使用所在类型中已声明的类型参数。
public T this [int index] => data [index];
类似的,构造函数可以参与使用已存在的类型参数,但不能引入新的类型参数:
public Stack<T>() { } // Illegal
4 声明类型参数
-
可以在声明类、结构体、接口、委托和方法时引入类型参数(Type Parameters)。其他的结构,如属性,虽不能引入类型参数,但也可以使用类型参数:
public struct Nullable<T> { public T Value { get; } }
-
泛型类型/泛型方法可以有多个参数:
class Dictionary<TKey, TValue> {...}
可以用以下方式实例化:
Dictionary<int, string> myDic = new Dictionary<int, string>();
或者:
var myDic = new Dictionary<int, string>();
-
只要类型参数的数量不同,泛型类型/泛型方法就可以重载。例如,以下三个类不会冲突:
class A {} class A<T> {} class A<T1, T2> {}
🌾 如果泛型类型/泛型方法只有一个类型参数且参数含义明确,一般将其命名为 T
。当使用多个类型参数时,每个类型参数使用 T 作为前缀,后面跟一个更具描述性的名称。
5 typeof 和未绑定泛型类型
🌚 我 🗼 🐴 也不知道以下这是什么玩意
在运行时不存在开放的泛型类型:开放泛型类型将汇编为程序的一部分而封闭。但运行时可能存在未绑定(unbound)的泛型类型,只作为 Type
对象存在。csharp 中唯一指定为绑定泛型类型的方式是使用 typeof
操作符:
class A<T> {}
class A<T1, T2> {}
...
Type a1 = typeof (A<>); // Unbound type (notice no type arguments)
Type a2 = typeof (A<, >); // Use commas to indicate multiple type args
开放泛型类型一般与反射 API 一起使用。🐴
也可以使用 typeof
操作符指定封闭的类型:
Type a3 = typeof (A<int, int>);
或一个开放类型:
class B<T> { void X() { Type t = typeof (T); } }
6 泛型的默认值
default
关键字可用于获取泛型类型参数的默认值。引用类型的默认值为 null
,值类型的默认值是将值类型的所有字段按位归零。
static void Zap<T> (T[] array)
{
for (int i = 0; i < array.Length; i++)
array[i] = default(T);
}
7 泛型的约束 Generic Constraints
默认情况下,泛型的类型参数(parameter)可以是任何类型的。
在类型参数上通过应用约束,可以将类型参数定义为指定的类型参数(argument):
约束 | 说明 |
---|---|
where T : base-class | Base-class constraint T 必须是名为 base-class 类的子类或是 base-class 类本身 |
where T : interface | Interface constraint T 必须实现名为 interface 的接口 |
where T : class | Reference-type constraint T 必须是引用类型。此约束还应用于任何类、接口、委托或数组类型。 |
where T : struct | Value-type constraint (excludes Nullable types) T 必须是值类型(不包括可空类型) |
where T : new() | Parameterless constructor constraint T 必须提供无参数的构造函数 |
where U : T | Naked type constraint U 必须继承 T |
如下,GenericClass<T, U>
的 T
要求派生于(或者本身就是) SomeClass
并且实现 Interface1
;要求 U
提供无参数构造函数:
class SomeClass {}
interface Interface1 {}
class GenericClass<T, U> where T : SomeClass, Interface1
where U : new()
{...}
🌾 约束可以应用在方法或者类型定义。
-
base-class 约束要求类型参数必须是子类(或者匹配父类);接口约束要求类型参数必须实现特定的接口。这些约束要求参数类型的实例可以隐式转换为相应的类和接口。例如,假定写一个泛型
Max
方法返回较大值。可以利用框架中定义的IComparable<T>
泛型接口:// .Net 已经自带的接口,编写代码时不用再次定义 public interface IComparable<T> // Simplified version of interface { int CompareTo (T other); // this 大于 other 时返回正值 }
以此接口为约束,将
Max
方法写为(此处省略null
检查):static T Max <T> (T a, T b) where T : IComparable<T> { return a.CompareTo (b) > 0 ? a : b; }
Max
方法可以接受任何实现了IComparable<T>
接口的类型参数,而 csharp 大部分内置类型如int
、string
都实现了该接口:int z = Max (5, 10); // 10 string last = Max ("ant", "zoo"); // zoo
-
类约束和结构体约束规定
T
必须是引用类型或值类型(不能为空),如System.Nullable<T>
结构体:struct Nullable<T> where T : struct { public T value {get;} public bool HasValue {get;} public T GetValueOrDefault(); public T GetValueOrDefault(T defaultValue); ... }
-
无构造函数约束要求
T
有一个无参数构造函数。如果定义了该约束,就可以调用new T()
:static void Initialize<T> (T[] array) where T : new() { for (int i = 0; i < array.Length; i++) array[i] = new T(); }
-
裸类型约束(naked type constraint)要求一个类型参数必须从另一个类型参数中派生(或匹配)。本例中,
FilteredStack
方法返回了另一个Stack
,返回的Stack
仅包含原来类中的一部分元素,并且类型参数U
是类型参数T
的子类。class Stack<T> { Stack<U> FilteredStack<U>() where U : T {...} }
8 继承泛型类型
-
泛型类和非泛型类一样都可以派生子类。并且子类或者中仍可以令基类中的类型参数保持开放,如下:
class Stack<T> {...} class SpecialStack<T> : Stack<T> {...}
-
子类也可以用具体的类来封闭泛型参数:
class IntStack : Stack<int> {...}
-
子类还可以引入新的类型参数:
class List<T> {...} class KeyedList<T, TKey> : List<T> {...}
🌾 技术上,子类中所有的类型参数都是新的:可以说子类封闭后又重新开放了父类的类型参数。这表明子类可以为其重新打开的类型参数使用更有意义的新名称:
class List<T> {...}
class KeyedList<TElement,TKey> : List<TElement> {...}
9 自引用泛型声明 Self-Referencing Generic Declarations
一个类型可以使用自身类型作为具体类型来封闭类型参数:
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> { ... }
10 静态数据
静态数据对于每一个封闭类型来说都是唯一的:
class Bob<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
}
}
11 类型参数的转换
csharp 的类型转换运算符可以进行多种的类型转换,包括:
- 数值转换
- 引用转换
- 装箱/拆箱转换
- 自定义转换(通过运算符重载)
-
根据已知操作数的类型,在编译时就已经决定了类型转换的方式。但是如果编译时操作数的类型还未确定,使得上述规则在泛型类型参数上会出现特殊的情形。如果导致了歧义,编译器会报错。如下:
StringBuilder Foo<T> (T arg) { if (arg is StringBuilder) return (StringBuilder) arg; // Will not compile ... }
由于不知道
T
的确切类型,编译器会怀疑你是否希望执行自定义转换。上述问题最简单的解决方案就是改用as
操作符,因为它不能进行自定义类型转换,因此是没有歧义的:StringBuilder Foo<T> (T arg) { StringBuilder sb = arg as StringBuilder; if (sb != null) return sb; ... }
更一般的做法是先将其转换为
object
类型,因为从object
转换,或将对象转换为object
都不是自定义转换,而是引用或者装箱/拆箱转换,StringBuilder
是引用类型,所以一定是引用转换:StringBuilder Foo<T> (T arg) { return (StringBuilder) (object) arg; }
-
拆箱转换也可能导致歧义。例如,下例中可能是拆箱转换、数值转换或者自定义转换:
int Foo<T> (T x) => (int) x; // Compile-time error
解决方案也是先将其转换为
object
类型 然后在将其转换为int
(很明显这是一个非歧义的拆箱转换):int Foo<T> (T x) => (int) (object) x;
12 协变、逆变、不变
12.1 协变 Covariance
- 假定
A
可以隐式引用转换为B
,如果X<A>
可以转换为X<B>
,称X
有一个协变类型参数。 - csharp 4.0 中,在接口和委托类型参数上指定
out
参数修饰符可以将其声明为协变参数,T
上的out
修饰符表明了T
只用于输出的位置(例如方法的返回值)。 - 方法中的
out
参数是不支持协变的,这是 CLR 的限制。 - 如果协变的类型参数出现在输入位置,例如方法的参数或可写属性,则会产生编译时错误。
12.2 逆变 Contravariance
- 假定
A
可以隐式引用转换为B
,如果X<B>
可以转换为X<A>
,称X
有一个逆变类型参数。 - 逆变参数仅出现在输入位置上,并用
in
修饰符标记才可以。 - 如果逆变的类型参数出现在输出位置,例如方法的返回值或可读属性,则会产生编译时错误。
12.3 举栗说明
🐴 以上内容完全不懂,简直像是看 cxk 🏀 🐔太美。
所以,以下是一些栗子🌰。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQpKtZFT-1596889482651)(Note_Figures/20200403164431551_26358.png)]
-
🌰 1
// 这样写是完全没问题 OK 滴 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; // 上面一句代码报错: // 无法将类型“System.Collections.Generic.IList<string>”隐式转换为“System.Collections.Generic.IList<object>”。存在一个显式转换(是否缺少强制转换?)
因为,假如上述代码是合法的,则:
IList<string> strings = new List<string> {"a", "b", "c"}; IList<object> objects = strings; objects.Add(new object()); // 转换成 object 类型,可以添加 new object() string element = string[3]; // 但实际上第四个元素是 object 类型,并不是 string 类型,引发了安全问题
进一步深究,查看
IEnumerable<T>
和IList<T>
(省略一堆我也不懂的玩意)的定义可以发现:-
IEnumerable<T>
定义,类型参数T
在接口中用作返回类型,作为输出(out
):// out T:T 作为输出 public interface IEnumerable<[NullableAttribute(2)] out T> : IEnumerable { [NullableContextAttribute(1)] IEnumerator<T> GetEnumerator(); }
-
IList<T>
定义,类型参数T
在接口中既有作为输入,也有作为输出:[DefaultMember("Item")] [NullableContextAttribute(1)] public interface IList<[NullableAttribute(2)] T> : ICollection<T>, IEnumerable<T>, IEnumerable { T this[int index] { get; set; } // T 作为输出 int IndexOf(T item); // T 作为输入 void Insert(int index, T item); void RemoveAt(int index); }
-
-
🌰 2
Action<object> objectAction = obj => Console.WriteLine(obj); Action<string> stringAction = objectAction; stringAction("Action<object> convert to Action<string>, it's a contravariance");
查看
Action<T>
的定义,类型参数T
在作为输入类型(in
):public delegate void Action<[NullableAttribute(2)] in T>(T obj);
12.4 言而总之
- Covariance 协变,类型参数作为返回值/
out
输出,子类转换为父类。
public interface IEnumerable<out T>
- Contravariance 逆变,类型参数作为输入值 input/
in
,父类转换为子类。
public delegate void Action<int T>
- Invariance 不变,类型参数既是输入,也是输出。
public interface IList<T>
天地玄妙无尽藏,星辰引渡一点光✨
- variance 只能出现在接口和委托里。
- variance 转换就是引用转换的一个例子。引用转换就是指无法改变其底层的值,只能改变编译时类型。例如对于
X<A>
和X<B>
,A
是B
的子类或A
实现了B
。而数值转换、装箱转换和自定义转换是不包含在内的。 - identity variance,本体转换,对 CLR 而言从一个类型转换到相同的类型(从
string
到string
,从object
到dynamic
(对 csharp 而言不是相同类型,但对 CLR 是相同类型))。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EJ42eJ5f-1596889482659)(Note_Figures/20200403164431551_26358.png)]
🌰1:-
合理的转换:
IEnumerable<string> to IEnumerable<object> // string to object 隐式转换,协变 IEnumerable<string> to IEnumerable<IConvertible> // string to IConvertible 实现接口 IEnumerable<IDisposable> to IEnumerable<object> Action<object> to Action<string> // string to object 隐式转换,逆变
-
不合理的转换:
IEnumerable<object> to IEnumerable<string> // 显示转换 IEnumerable<string> to IEnumerable<Stream> // 毫无关系 IEnumerable<int> to IEnumerable<IConvertible> // 装箱转换 IEnumerable<int> to IEnumerable<long> // 数值转换
-
🌰2:
static void Main(string[] args)
{
IEnumerable<string> strings = new[] {"a", "b", "cdefg", "hij"};
// 筛选出字符串长度大于 1 的
List<object> list = strings
.Where(x => x.Length > 1)
.ToList();
// 无法将类型“System.Collections.Generic.List<string>”隐式转换为“System.Collections.Generic.List<object>”
}
-
因为
List<T>
是不变的,所以无法将List<string>
转换为List<object>
,此时可以:IEnumerable<string> strings = new[] {"a", "b", "cdefg", "hij"}; // 筛选出字符串长度大于 1 的 List<object> list = strings .Where(x => x.Length > 1) .Cast<object>() // 返回一个 IEnumerable<object> .ToList();
Cast<T>
的原型:public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source);
-
或者直接写成:
IEnumerable<string> strings = new[] {"a", "b", "cdefg", "hij"}; // 筛选出字符串长度大于 1 的 List<object> list = strings .Where(x => x.Length > 1) .ToList<object>();
ToList<T>
的原型:public static List<TSource> Cast<TSource>(this IEnumerable<TSource> source);
其中,
Where(x => x.Length > 1)
的输出为一个IEnumerable<string>
类型,所以到ToList<T>
将其通过协变转换为了IEnumerable<object>
,然后再通过ToList<T>
得到了List<object>
。
🌴csharp 的泛型,生产类型(例如 List<T>
)可以被编译到 dll 里,因为这种在生产者和产制封闭类型的消费者之间的合成是发生在运行时的。
12.5 树上的栗子
如下:
public class Animal {}
public class Bear : Animal {}
public class Camel : Animal {}
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
接下来的语句是不能通过编译的:
Stack<Bear> bears = new Stack<Bear>();
Stack<Animal> animals = bears; // Compile-time error
这种约束避免了以下代码可能产生的运行时错误:
animals.Push (new Camel()); // Trying to add Camel to bears
但是协变的确缺失可能会妨碍复用性。例如,我们希望写一个 Wash
方法操作整个 Animal
栈,但因为 Stack<Bear>
无法转换为 Stack<Animal>
,将 Stack<Bear>
传入 Wash
方法会产生编译时错误:
public class ZooCleaner
{
public static void Wash(Stack<Animal> animals) {...}
}
-
一个解决方法是重新定义一个带有约束的
Wash
方法:class ZooCleaner { public static void Wash<T>(Stack<T> animals) where T : Animal { ... } }
这样我们就可以调用
Wash
了:Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash (bears);
-
另一种解决方案是让
Stack<T>
实现一个拥有协变类型参数的泛型接口。假定Stack<T>
类实现了如下接口:public interface IPoppable<out T> { T Pop(); }
T
上的out
修饰符表明了T
只用于输出的位置(例如方法的返回值)。out
修饰符将类型参数标记为协变参数,并且可以进行如下的操作:var bears = new Stack<Bear>(); bears.Push (new Bear()); // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>: IPoppable<Animal> animals = bears; // Legal Animal a = animals.Pop();
如前所述,可以利用类型转换的协变性解决复用性问题:
public class ZooCleaner { public static void Wash (IPoppable<Animal> animals) { ... } }