C#中的泛型
一、什么是泛型
- 泛型可以让多段代码在不同的数据类型上执行相同的指令。
- 泛型允许我们声明类型参数化的代码,可以用不同的类型进行实例化,在创建类型时才指明真实的类型。
- 泛型是类型的模板,类型是实例的模版。
- C#提供了五种泛型:类、结构、接口、委托和方法。(前四个是类型,方法是成员)
二、泛型类
1. 声明泛型类
- 在类名之后放一组尖括号。
- 在尖括号中使用逗号分隔的占位符字符串来表示希望提供的类型,这叫做类型参数。
- 在泛型类声明的主体中使用类型参数来表示应该替代的类型。
2. 创建构造类型
- 在创建构造类型时,需要将泛型类的类型占位符代替为真实的类型实参。
- 类型参数:泛型类上声明来用作类型的占位符。
- 类型实参:在创建构造类型时提供的真实类型。
3. 使用泛型的模拟栈的示例
使用泛型模拟栈,感受泛型带来的便捷,同一段代码让不同类型来使用。
//栈class MyStack<T>{T[] StackArray;int StackPointer = 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 Push(T t){if (!IsStackFull)StackArray[StackPointer++] = t;}public T pop() {return (!IsStackEmpty) ? StackArray[--StackPointer] : StackArray[0];}public void Print() {for (int i = StackPointer-1; i >=0; i--)Console.WriteLine(StackArray[i]);}}internal class Program{static void Main(string[] args){MyStack<int> intStack = new MyStack<int>();MyStack<string> strStack = new MyStack<string>();intStack.Push(1);intStack.Push(2);intStack.Push(3);intStack.Push(4);intStack.Print();Console.WriteLine("-----------");strStack.Push("熊大");strStack.Push("熊二");strStack.Push("光头强");strStack.Push("吉吉国王");strStack.Print();}}
4. 比较泛型栈和非泛型栈
非泛型 | 泛型 | |
源代码大小 | 更大:需要为每一种类型编写新的实现 | 更小:不管构造类型数量有多少,只需要一个实现 |
可执行大小 | 每一种类型的栈都被编译 | 只会出现有构造类型的类型 |
书写难易度 | 更具体,易于书写 | 更抽象,不易于书写 |
维护难易度 | 容易出问题,需要修改所有类型 | 易于维护,只需要修改一个地方 |
三、类型参数的约束
- 要让泛型变得更有用,我们需要提供额外的信息让编译器知道参数可以接受哪些类型,这些额外的信息叫做约束。
- 只有符合约束的类型才能替代给定的类型参数,来产生构造类型。
1. where子句
- 约束使用where子句列出。每一个有约束的类型参数有自己的约束语句。
- 如果形参有多个约束,它们在where子句中使用逗号分隔。
- where子句语法如下:
2. 约束类型和次序
四、泛型方法
- 与其他泛型不一样,方法是成员,不是类型。
- 泛型方法可以在泛型或非泛型的类以及结构和接口中声明。
1. 声明泛型方法
- 泛型方法包含类型参数列表,方法参数类表,和可选的约束子句。
- 返回值位置也可以使用泛型占位符占位。
2. 调用泛型方法
要调用泛型方法,应该在方法调用时提供类型参数。
五、扩展方法和泛型类
扩展方法可以泛型类相结合。
- 和非泛型类一样,泛型类的扩展方法也必须声明为static
- 扩展方法必须是静态类的成员
- 第一个参数类型中必须有this关键字,后面是扩展的泛型类的名字
代码示例:
static class ExtendHolder {//扩展方法:被扩展的构造类型可以像使用自己的方法一样使用扩展方法public static void Print<T>(this Holder<T> h) {T[] vals = h.GetValues();Console.WriteLine("{0},{1},{2}", vals[0], vals[1],vals[2]);}}class Holder<T> {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; }}internal class Program{static void Main(string[] args){Holder<int> intHolder = new Holder<int>(5, 6, 7);Holder<string> strHolder = new Holder<string>("a1", "a2", "a3");intHolder.Print();strHolder.Print();}}
六、泛型结构
泛型结构的规则和条件和泛型类是一样的
struct MyData<T> {private T _data;public T Data {get { return _data; }set { _data = value; }}public MyData(T t) {_data = t;}}internal class Program{static void Main(string[] args){MyData<int> myIntData = new MyData<int> (10);MyData<string> myStringData = new MyData<string>("懒羊羊");Console.WriteLine(myIntData.Data);Console.WriteLine(myStringData.Data);}}
七、泛型委托
//泛型委托public delegate TR Func<T1, T2, TR>(T1 t1, T2 t2);class Simple {public static string PrintString(int p1, int p2) {int total = p1 + p2;return total.ToString();}}internal class Program{static void Main(string[] args){Func<int, int, string> func = new Func<int, int, string>(Simple.PrintString);Console.WriteLine("Total : {0}",func(6,7));}}
八、泛型接口
- 泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。
- 实现不同类型参数的泛型接口是不同的接口。
- 可以在非泛型类型中实现泛型接口。
- 泛型接口的实现必须唯一。
- 泛型接口的名字不会和非泛型接口名字冲突。
interface IMyIfc<T> {T ReturnIt(T t);}class Simple : IMyIfc<int>, IMyIfc<string> //实现同一个泛型接口的两个不同类型参数的接口{public int ReturnIt(int t) //实现int类型接口{return t;}public string ReturnIt(string t) //实现string类型接口{return t;}}internal class Program{static void Main(string[] args){Simple simple = new Simple();Console.WriteLine("{0}", simple.ReturnIt(5));Console.WriteLine("{0}", simple.ReturnIt("沸羊羊"));}}
九、可变性
1. 协变
C#中派生类实例对象可以赋给基类变量,这叫做赋值兼容性。但是试图将派生类的泛型委托赋值给基类泛型委托变量就会出错。
原因是两个委托之间没有继承关系,所以赋值兼容性不适用。
如果派生类只是用作输出值,那么这种结构化的委托有效性之间的常数关系叫做协变。当我们将案例中的委托声明使用out关键字改为输出参数,就可以完成协变。
代码示例如下:
delegate T Factory<out T>();//委托class Animal { public int Legs = 4; } //动物类class Dog : Animal { } //狗类internal class Program{static Dog MakeDog() { return new Dog();}static void Main(string[] args){Factory<Dog> dog = MakeDog;Factory<Animal> animal = dog;Console.WriteLine(animal().Legs.ToString());//代码调用可以正常操作Animal部分}}
2. 逆变
在使用泛型委托时,期望传入基类时允许传入派生类对象的特性叫做逆变。使用关键字in。
delegate void Factory<in T>(T t);//委托class Animal { public int Legs = 4; } //动物类class Dog : Animal { } //狗类internal class Program{static void ActOnAnimal(Animal a) { Console.WriteLine(a.Legs); }static void Main(string[] args){Factory<Animal> act1 = ActOnAnimal;Factory<Dog> dog = act1;dog(new Dog());}}
协变与逆变的比较理解:
3. 补充
- 泛型接口也可以协变和逆变,与泛型委托类似。
- 协变与逆变处理的是派生类和基类之间安全情况,只适用于引用类型,值类型不行。
- 显示变化in和out只适用于委托和接口,不适用于类、结构和方法。
- 不包括in和out关键字的委托和接口类型参数叫不变,这些类型参数不能用于协变或逆变。
(注:本章学习总结自《C#图解教程》)