第18章 泛型 笔记18.1 什么是泛型泛型可以将重构代码并且额外添加一个抽象层是专门为多段代码在不同的数据类型上执行相同指令而设计的。18.2 C# 中的泛型泛型可以让多个类型共享一组代码允许声明类型参数化代码用不同的类型进行实例化。泛型不是类型而是类型的模板。C# 提供了以下 5 种泛型类结构接口委托方法其中 1 ~ 4 是类型5 是成员。18.3 泛型类泛型类不是实际的类而是类的模板必须先从它们构建实际的类然后创建类的引用和实例。1.在某些类型上使用一个占位符来声明一个类。2.为占位符提供真实类型构造类型。3.创建构造类型的实例。18.3.1 声明泛型类声明语法1.在类名之后放置一组尖括号。2.在尖括号中用逗号分隔占位符字符串用于表示需要提供的类型类型参数。3.在泛型类声明的主体中使用类型参数来表示替代类型。class SomeClass T1, T2 { // Normally, a type would be used in this position. public T1 SomeVar; public T2 OtherVar; }18.3.2 创建构造类型声明泛型类后可以告诉编译器使用哪些真实类型来替代占位符编译器将获取这些真实类型并创建构造类型用来创建真实类对象的模板。SomeClassshort, int泛型类声明上的类型参数用作类型的占位符。在创建构造类型时提供的真实类型是类型实参。// 类型参数 class SomeClassT1, T2 { //... } // 类型实参 SomeClassshort, int18.3.3 创建变量和实例类对象的创建MyNonGenClass myNGC new MyNonGenClass (); // Constructed class Constructed class SomeClassshort, int mySc1 new SomeClassshort int(); var mySc2 new SomeClassshort, int();和非泛型类一样引用和实例可以分开创建。// 泛型类 class SomeClassT1, T2 { public T1 SpmeVar; public T2 OtherVar; } // 分配类变量 SomeClass short, int myInst; // 分配实例 myInst new SomeClassshort, int ();18.3.4 使用泛型的示例使用泛型来实现栈的示例class MyStackT { T[] StackArray; int StackPointer 0; public void Push(T x) { if ( !IsStackFull ) StackArray[StackPointer] x; } public T Pop() { return ( !IsStackEmpty ) ? StackArray[--StackPointer] : StackArray[0]; } const int MaxStack 10; bool IsStackFull { get{ return StackPointer MaxStack; } } bool IsStackEmpty { get{ return StackPointer 0; } } public MyStack() { StackArray new T[MaxStack]; } public void Print() { for (int i StackPointer-1; i 0 ; i--) Console.WriteLine($ Value: { StackArray[i] }); } } class Program { static void Main( ) { MyStackint StackInt new MyStackint(); MyStackstring StackString new MyStackstring(); StackInt.Push(3); StackInt.Push(5); StackInt.Push(7); StackInt.Push(9); StackInt.Print(); StackString.Push(This is fun); StackString.Push(Hi there! ); StackString.Print(); } } // output Value: 9 Value: 7 Value: 5 Value: 3 Value: Hi there! Value: This is fun18.3.5 比较泛型和非泛型栈非泛型栈和泛型栈之间的区别非泛型泛型源代码大小更大需要为每一种类型编写一个新的实现更小不管构造类型的数量有多少只需要一个实现可执行文件大小无论每一个版本的栈是否会被使用都会在编译的版本中出现可执行文件中只会出现有构造类型的类型写的难易度易于书写因为它更具体比较难写因为它更抽象维护的难易度更容易出问题因为所有修改需要应用到每一个可用的类型上易于维护因为只需要修改一个地方18.4 类型参数的约束符合约束的类型参数叫做未绑定的类型参数要让泛型更加有用需要提供额外的信息让编译器直到参数可以接受哪些类型这些额外的信息称为约束。18.4.1 Where 子句每个有约束的类型参数都有自己的 where 子句。如果形参有多个约束则使用逗号分隔。// 类型参数 约束列表 where TypeParam : constraint, constraint, ...有关 where 子句的要点如下在类型参数列表的关闭尖括号后列出。不使用分隔符。可以随意次序列出。where 是上下文关键字可以在其他上下文使用。// T2T3 有约束并且没有分隔符 class MyClass T1, T2, T3 where T2: Customer // Constraint for T2 where T3: IComparable // Constraint for T3 { // ... }18.4.2 约束类型和次序5种类型的约束约束类型描述类名 class只有这个类型的类或从它派生的类才能用作类型实参struct任何值类型都可以用作类型实参接口名只有这个接口或实现这个接口的类型才能用作类型实参new()任何带有无参公共构造函数的类型都可以用作类型实参。这叫作构造函数约束where子句可以任意次序子句中的约束必须具有特定的顺序。最多只能有一个主约束必须放在第一位。可以有任意个接口名称约束。如果存在构造函数约束必须放在最后。如果类型参数有多个约束则必须遵守的顺序class SortedListS where S: IComparableS { ... } class LinkedListM,N where M : IComparableM where N : ICloneable { ... } class MyDictionaryKeyType, ValueType where KeyType : IEnumerable, new() { ... }18.5 泛型方法泛型方法可以在泛型 / 非泛型类、结构和接口中声明。18.5.1 声明泛型方法泛型方法具有类型参数列表和可选的约束泛型方法有两个参数列表。方法参数列表圆括号内。类型参数列表尖括号内。方法参数列表后放置可选的约束子句。// 类型参数列表 方法参数列表 约束子句 public void PrintDataS, T ( S p, T t ) where S: Person { // ... }18.5.2 调用泛型方法MyMethodshort, int(); MyMethodint, long ();编译器使用每个构造函数实例产生方法的不同版本。编译器有时可以从方法参数推断类型参数。例如对于如下的方法声明public void MyMethod T (T myVal) { ... }编译器可以从 myInt 参数的类型推断出 T 为 int因此可以省略尖括号。int myInt 5; MyMethod int (myInt);18.5.3 泛型方法的示例class Simple // Non-generic class { static public void ReverseAndPrintT(T[] arr) // Generic method { Array.Reverse(arr); foreach (T item in arr) // Use type argument T. Console.Write( ${ item.ToString() }, ); Console.WriteLine(); } } class Program { static void Main() { // Create arrays of various types. var intArray new int[] { 3, 5, 7, 9, 11 }; var stringArray new string[] { first, second, third }; var doubleArray new double[] { 3.567, 7.891, 2.345 }; Simple.ReverseAndPrintint(intArray); // Invoke method. Simple.ReverseAndPrint(intArray); // Infer type and invoke. Simple.ReverseAndPrintstring(stringArray); // Invoke method. Simple.ReverseAndPrint(stringArray); // Infer type and invoke. Simple.ReverseAndPrintdouble(doubleArray); // Invoke method. Simple.ReverseAndPrint(doubleArray); // Infer type and invoke. } } // output 11, 9, 7, 5, 3, 3, 5, 7, 9, 11, third, second, first, first, second, third, 2.345, 7.891, 3.567, 3.567, 7.891, 2.345,18.6 扩展方法和泛型类和非泛型类一样泛型类的扩展方法必须满足如下条件声明为 static。是静态类的成员。第一个参数类型中必须有关键字 this后面是扩展的泛型类的名字。Print扩展了 Holder泛型类static class ExtendHolder { public static void PrintT(this HolderT h) { T[] vals h.GetValues(); Console.WriteLine(${ vals[0] },\t{ vals[1] },\t{ vals[2] }); } } class HolderT { T[] Vals new T[3]; public Holder(T v0, T v1, T v2) { Vals[0] v0; Vals[1] v1; Vals[2] v2; } public T[] GetValues() { return Vals; } } class Program { static void Main(string[] args) { var intHolder new Holderint(3, 5, 7); var stringHolder new Holderstring(a1, b2, c3); intHolder.Print(); stringHolder.Print(); } } // output 3, 5, 7 a1, b2, c318.7 泛型结构泛型结构的规则和条件与泛型类一致。struct PieceOfDataT // Generic struct { public PieceOfData(T value) { _data value; } private T _data; public T Data { get { return _data; } set { _data value; } } } class Program { static void Main() Constructed type { var intData new PieceOfDataint(10); var stringData new PieceOfDatastring(Hi there.); Constructed type Console.WriteLine($intData { intData.Data }); Console.WriteLine($stringData { stringData.Data }); } } // output intData 10 stringData Hi there.18.8 泛型委托声明泛型委托// 返回类型 类型参数 委托形参 delegate R MyDelegateT, R( T value );有两个参数列表委托形参列表和类型参数列表类型参数的范围包括返回类型、形参列表、约束子句delegate void MyDelegateT(T value); // Generic delegate class Simple { static public void PrintString(string s) // Method matches delegate { Console.WriteLine( s ); } static public void PrintUpperString(string s) // Method matches delegate { Console.WriteLine(${ s.ToUpper() }); } } class Program { static void Main( ) { var myDel // Create inst of delegate. new MyDelegatestring(Simple.PrintString); myDel Simple.PrintUpperString; // Add a second method. myDel(Hi There.); // Call delegate. } } // output Hi There. HI THERE.C# LINQ 特性大量使用泛型委托。public delegate TR FuncT1, T2, TR(T1 p1, T2 p2); // Generic delegate class Simple Delegate return type { static public string PrintString(int p1, int p2) // Method matches delegate { int total p1 p2; return total.ToString(); } } class Program { static void Main() { var myDel // Create inst of delegate. new Funcint, int, string(Simple.PrintString); Console.WriteLine($Total: { myDel(15, 13) }); // Call delegate. } } // output Total: 2818.9 泛型接口泛型接口的声明和非泛型接口的声明类似但是要在接口名称后的尖括号中放置类型参数。interface IMyIfcT // Generic interface { T ReturnIt(T inValue); } class Simple : IMyIfcint, IMyIfcstring // Nongeneric class { public int ReturnIt(int inValue) // Implement interface using int. { return inValue; } public string ReturnIt(string inValue) // Implement interface using string. { return inValue; } } class Program { static void Main() { Simple trivial new Simple(); Console.WriteLine(${ trivial.ReturnIt(5) }); Console.WriteLine(${ trivial.ReturnIt(Hi there.) }); } } // output 5 Hi there.18.9.1 使用泛型接口的示例另外两项能力用不同类型的参数实例化的泛型接口的实例是不同的接口可以在非泛型类型中实现泛型接口18.9.2 泛型接口的实现必须唯一必须保证类型实参的组合 不会在类型中产生两个重复的接口。例如对于下面的泛型接口会产生潜在的冲突S 可能用作 int 类型此时会有两个相同类型的接口这将不被允许。interface IMyIfcT { T ReturnIt(T inValue); } // Two interfaces class SimpleS : IMyIfcint, IMyIfcS // Error! { public int ReturnIt(int inValue) // Implement first interface. { return inValue; } public S ReturnIt(S inValue) // Implement second interface, { // but if its int, it would be return inValue; // the same as the one above. } }泛型结构的名称不会和非泛型冲突。18.10 协变和逆变可变性分为三种协变、逆变、不变18.10.1 协变out将派生类型的对象赋值给基类型的变量叫做 赋值兼容性给出如下例子class Animal { public int NumberOfLegs 4; } class Dog : Animal { } class Program { static void Main( ) { Animal a1 new Animal( ); Animal a2 new Dog( ); Console.WriteLine($Number of dog legs: { a2.NumberOfLegs }); } } // output Number of dog legs: 4Dog类型的变量可以作为Animal类型的引用因为Dog由Animal派生而来发生了隐式类型转换。进行扩展添加 Factory 泛型委托、MakeDog 方法并且 MakeDog 方法可以匹配 Factory 委托。class Animal { public int Legs 4; } // Base class class Dog : Animal { } // Derived class delegate T FactoryT( ); // delegate Factory class Program { static Dog MakeDog( ) // Method that matches delegate Factory { return new Dog( ); } static void Main( ) { FactoryDog dogMaker MakeDog; // Create delegate object. FactoryAnimal animalMaker dogMaker; // Attempt to assign delegate object. Console.WriteLine( animalMaker( ).Legs.ToString( ) ); } }Main 函数的第二行尝试将FactoryDog类型赋给FactoryAnimal类型这将产生报错。问题的原因在于委托FactoryDog并没有从FactoryAnimal派生得到。赋值兼容性不适用因为两个委托没有继承关系仅希望传递Dog给FactoryAnimal委托时代码对Dog类型中的Animal部分进行操作这并不会发生越界访问是完全合理的。为了完成我们的期望可以通过添加 out 关键字改变委托声明。delegate T Factoryout T( );协变关系允许程度更高的派生类型处于返回及输出位置18.10.2 逆变逆变基类 → 派生类与协变相反如果类型参数只用于方法中的输入参数那么可以传入更高程度的派生类引用因为委托的方法中只对其基类部分进行操作。class Animal { public int NumberOfLegs 4; } class Dog : Animal { } class Program { // Keyword for contravariance delegate void Action1in T( T a ); static void ActOnAnimal( Animal a ) { Console.WriteLine( a.NumberOfLegs ); } static void Main( ) { Action1Animal act1 ActOnAnimal; Action1Dog dog1 act1; dog1( new Dog() ); } } // output 4调用委托时调用代码为方法 ActOnAnimal 传入的 Dog 类型的变量而其期望的是 Animal 对象因此可以进行操作。逆变允许程度更高的派生类型作为输入参数18.10.3 协变和逆变的不同18.10.4 接口的协变和逆变相同的原则也适用于接口。class Animal { public string Name; } class Dog: Animal{ }; // Keyword for covariance interface IMyIfcout T { T GetFirst(); } class SimpleReturnT: IMyIfcT { public T[] items new T[2]; public T GetFirst() { return items[0]; } } class Program { static void DoSomething(IMyIfcAnimal returner) { Console.WriteLine( returner.GetFirst().Name ); } static void Main( ) { SimpleReturnDog dogReturner new SimpleReturnDog(); dogReturner.items[0] new Dog() { Name Avonlea }; IMyIfcAnimal animalReturner dogReturner; DoSomething(dogReturner); } } // output Avonlea18.10.5 关于可变性的更多内容实际上编译器可以自动识别某个已构建的委托是协变还是逆变并且自动进行类型强制转换但这通常发生在没有为对象的类型赋值的时候。Main 第一行创建了FactoryAnimal类型的委托并直接将方法 MakeDog 赋值给它。由于没有创建FactoryDog委托因此编译器清楚这是协变关系允许这种赋值哪怕委托中没有 out 标识符。到 Main 第三行时由于第二行已经创建了FactoryDog委托因此后面的协变关系赋值需要 out 标识符才能完成。class Animal { public int Legs 4; } // Base class class Dog : Animal { } // Derived class class Program { delegate T Factoryout T(); static Dog MakeDog() { return new Dog(); } static void Main() { FactoryAnimal animalMaker1 MakeDog; // Coerced implicitly FactoryDog dogMaker MakeDog; FactoryAnimal animalMaker2 dogMaker; // Requires the out specifier FactoryAnimal animalMaker3 new FactoryDog(MakeDog); // Requires the out specifier } }重要事项可变性只适用于引用类型不适用值类型。in、out 关键字的显式变化只适用于委托和接口不适用于类、结构和方法。不使用 int、out 关键字的委托和接口类型参数是不变的。// 协变 逆变 delegate T Factoryout R, in S, T( );