目录
1.泛型的意义
泛型编程最初诞生于C++中,由Alexander Stepanov[2]和David Musser[3]创立。目的是为了实现C++的STL。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。
所以如果你学过C++,那么你一定很熟悉泛型的概念,C++中常说的模板就是泛型。为了便于初学者理解,我们举个具体的例子。假设我们要创建一个栈类,,该类实现了一个int型的栈,允许我们把int压入栈中,以及把他们弹出。
class MyIntStack
{
int StackPoint=0;
int[] StackAraay; //int 型数组
public void Push(int x)
{
.....
}
public int Pop()
{
.....
}
}
考虑到我可能需要实现其它类型如float类型的栈,那简单的做法就是讲上面的定义复制一份,然后将int替换为float,再把类名改为MyFloatStack。就是这样:
class MyFloatStack
{
int StackPoint=0;
float[] StackAraay; //int 型数组
public void Push(float x)
{
.....
}
public float Pop()
{
.....
}
}
虽然复制粘贴很方便,但很容易出错,且有如下缺点:
- 你需要仔细检查类的每一个部分来看哪些类型的声明需要修改,哪些类型的声明需要保留
- 每次需要更新的时候,需要重复这个过程
- 在这个过程后,我们有了很多几乎相同的代码副本,我们都需要重复这个过程
- 会增加调试和维护的难度
对于int或者float,除了类型不一样,累的方法,字段都是一样的,入股我们能将类型抽象出来,类似于数学中用x代表一个数,这样,当需要用到x时,只需将x用具体的数值替代就可以了。
2.C#中的泛型
可以将泛型类型理解为泛型模板:
C#提供了5种泛型:类,结构,接口,委托和方法,除了方法,前面4个都是类型。现在我们要对前一节中的类进修改是的它变成一个 泛型类:
-
在MyIntSatck中,使用类型占位符T而不是floatl来替换int
-
修改类名为MyStack
-
在类名后放置<T>
结构就是如下的泛型类声明
class MyStack <T>
{
int StackPoint=0;
T [] StackAraay;
public void Push(T x) {...}
public T Pop() {...}
}
对于泛型类,创建对象方式与非泛型类创建对象类似:
非泛型类:MyNonGenClass myNGC=new MyNonGenClass
泛型类:SomeClass<short,int> mySc1=new SomeClass<short,int>();
上面一步还可以简化一下为: var mySc1=new SomeClass<short,int>();
下面给出一个使用泛型栈的例子
namespace StackTest
{
class MyStack<T>
{
T[] StackArray;
int StackPointer = 0;
public void Push(T x)
{
if (!IsStackFull)
StackArray[StackPointer++] = x;
}
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 = 0; i < StackPointer; i++)
Console.WriteLine(" Value:{0}", StackArray[i]);
}
}
class Program
{
static void Main(string[] args)
{
MyStack<int> StackInt = new MyStack<int>();
MyStack<string> StackString = new MyStack<string>();
StackInt.Push(3);
StackInt.Push(4);
StackInt.Push(44);
StackInt.Push(9);
StackInt.Print();
StackString.Push("This is fun!");
StackString.Push("Hi, there!");
StackString.Print();
}
}
}
泛型类型参数的约束
理论上,类型替代符T可以替代任何前面提到的五种类型,然后有时候需要加入一些约束,来确保泛型可以正常运行。例如下面的一个例子:
class Simple<T>
{
Static public bool LessThan(T i1,T i2)
{
return i1<i2;
}
}
函数LessThan用小与运算符返回结果,但是不是所有的类都实现了小与运算符,也就不能用任何类来替代T,所以编译器会产生一个错误信息。
约束使用where子句,其中:
- 每一个有约束的类型参数有自己的where子句
- 如果有多个约束,它们在where子句中使用逗号分隔。
语法如下:
where TypeParam(类型参数) : constraint, constraint,...
- 它们在类型参数列表的关闭尖括号之后列出
- 它们不使用逗号或其它分隔符
- 它们可以以任何次序列出
- where是上下文关键字,所以可以在其他上下文中使用
class Myclass<T1,T2,T3>
where T2: Customer
where T3: IComperable
{
...
}
3.泛型方法
泛型方法声明如下:
public void PrintData<S,T>(S p,T t) where S:Person
{
}
要注意类型参数,方法参数,约束的顺序
在调用泛型方法时,应该在方法调用时提供类型实参,如下所示:
MyMethod<short,int>();
编译器有时可以从方法参数中推断出泛型方法的类型形参中用到的哪些类型,这样就可以使调用方法更简单。例如:
public void Method<T> (T val) {...}
因为两个都是T类型,所以当我么调用时,使用int,编译器就可以推断出来
Method<int> (5) 可以写成 Method(5)
下面给出一个具体的使用例子:
using System;
namespace TestGen
{
class Simple
{
static public void ReverseAndPrint<T>(T[] array)
{
Array.Reverse(array);
foreach (T item in array)
Console.Write("{0},", item.ToString());
Console.WriteLine("");
}
}
class MainClass
{
public static void Main(string[] args)
{
var intArray = new int[] { 3, 5, 7, 9, 11 };
var stringArray = new string[] { "fisrt", "second", "third" };
var doubleArray = new double[] { 1.2, 12.3, 3.45, 3.9 };
Simple.ReverseAndPrint<int>(intArray);
Simple.ReverseAndPrint(intArray); //推断类型并调用
Simple.ReverseAndPrint<string>(stringArray);
Simple.ReverseAndPrint(stringArray);
Simple.ReverseAndPrint<double>(doubleArray);
Simple.ReverseAndPrint(doubleArray);
}
}
}
产生的结果如下:
4.泛型委托
泛型委托和非泛型委托非常相似,不过类型参数决定了能接受什么样的方法。泛型委托声明语法如下:
delegate R MyDelegate<R,T>(T value)
下面给一个具体的实例
using System;
namespace TestGenDelegates
{
delegate void MyDelegate<T>(T value);
class Simple
{
static public void PrintString(string s)
{
Console.WriteLine(s);
}
static public void PrintUpperString(string s)
{
Console.WriteLine("{0}", s.ToUpper());
}
}
class MainClass
{
public static void Main(string[] args)
{
var myDel = new MyDelegate<string>(Simple.PrintString);
myDel += Simple.PrintUpperString;
myDel("Hi, Look at you, you did good!");
}
}
}
C#的·LINQ特性在很多地方使用了泛型委托,学习后面的linq时还会接触委托泛型。
5.泛型接口
泛型接口和非泛型接口也差不多,但是需要在接口名称后面放置类型参数。还是先看一个小例子:
using System;
namespace TestGenInterface
{
interface IMyIfc<T>
{
T ReturnIt(T invalue);
}
class Simple<S>: IMyIfc<S>
{
public S ReturnIt(S invalue)
{
return invalue;
}
}
class MainClass
{
public static void Main(string[] args)
{
var trivInt = new Simple<int>();
var trivString = new Simple<string>();
Console.WriteLine("{0}", trivInt.ReturnIt(5));
Console.WriteLine("{0}", trivString.ReturnIt("Hi ,There"));
}
}
}
上面的例子分别实现了2个接口类型,我们的Simple类也是一个泛型类,其实并不一定需要泛型类,看下面一个例子:
class Simple2: IMyIfc<int>,IMyIfc<string>
{
public int ReturnIt(int invalue)
{
return invalue;
}
public string ReturnIt(string invalue)
{
return invalue;
}
}
class MainClass
{
public static void Main(string[] args)
{
//var trivInt = new Simple<int>();
//var trivString = new Simple<string>();
Simple2 trivial = new Simple2();
Console.WriteLine("{0}", trivial.ReturnIt(5));
Console.WriteLine("{0}", trivial.ReturnIt("Hi ,There"));
}
}
}
实现泛型接口事,必须保证类型参数组合不会再类型中产生两个重复的接口。例如在下点的代码中,Simple使用了两个IMyIfc接口的实例化,使用两个接口本身没有问题,问题在于这么做会产生一个潜在的冲突,因为如果把int作为类型参数来替代第二个接口中的S的话,Simple可能会有两个相同类型的接口这是不允许的。
interface IMyIfc<T>
{
T ReturnIt(T value);
}
class Simple<S>: IMyIfc<int>,IMyIfc<S>
{
public int ReturnIt(int value)
{
return value;
}
public S ReturnIt(S value)
{
return value;
}
}
C# 泛型中还有2个概念:协变和逆变需要了解,为了更好的讲解,我把它门两个单独做一篇来介绍。