1.1.1 摘要
图1 C# 泛型介绍
在接触泛型之前,我们编程一般都是使用具体类型(char, int, string等)或自定义类型来定义我们变量,如果我们有一个功能很强的接口,而且我们想把它提取或重构成一个通用的接口,使得该接口不仅仅适用于已定义数据类型,而是适用于更多数据类型,从而方便以后的扩展。
泛型提供上述功能的实现,泛型其实就是提供一个数据类型的抽象层,因为它泛所以抽象,方便了我们代码的重构和提取,我们无需hard-code接口中的数据类型,而是通过一个抽象泛型类型来指定数据类型,所以泛型可以提取出一个通用的接口。接下来让我们通过具体的例子说明什么是泛型。
1.1.2 正文
1.1.1.1 没有泛型的C# 1.0
相信许多人都使用过栈或者其他经典数据结构,而且对于栈的FILO都是娴熟于心了。假设现在要我们定义一个栈,用来存放具体数据类型,例如:int 数据,OK让我们来实现自定义栈。
/// <summary> /// A custom stack1. /// </summary> public class CustomStack1 { private int _stackPointer = 0; private int[] _stackArray; public void Push(int item) { ////Concrete implemention. } public int Pop() { ////Concrete implemention. } }
为了简洁没有完全遵守OOP的原则,这里没有使用属性和给出方法的具体实现,现在我们有了第一版可以存放int数据类型的栈。但如果需求改变保存数据类型增加string。那么我们可以怎样实现呢?没错我们只要增加一个类型为string的栈就OK了。
/// <summary> /// A custom stack2. /// </summary> public class CustomStack2 { private int _stackPointer = 0; private string[] _stackArray; public void Push(int item) { ////Concrete implemention. } public string Pop() { ////Concrete implemention. } }
很快我们就完成了第二版本的栈了,这看上去的确很简单而且实现起来很快只不过是ctrl + c和ctrl + v,但我们的代码真的那么简单好维护吗?想想其实危机四伏。我们能不能一开始就把保存的数据类型定义成为一个通用的类型呢(抽象数据类型)?----邪恶的object。
/// <summary> /// A custom stack. /// </summary> public class CustomStack { private int _stackPointer = 0; private object[] _stackArray; public void Push(object item) { ////Concrete implemention. } public object Pop() { ////Concrete implemention. } }
现在我们实现了第三版的栈了,看上去我们好像找到了通用的解决方法,想想我们要注意一点是如果我们保存和取出数据类型为值类型时,值类型要进行boxing和unboxing操作(《.NET值类型和引用类型101》),而且还有进行数据类型检测。如果数据量不大我们可能察觉不到性能变化,但数据量大hehehe…。这就是为什么我们要使用泛型的原因之一,但更重要的原因是在进行boxing和unboxing时,我们需要提供更多信息给编译器,编译时再根据我们提供的信息进行类型检测。
1.1.1.2 C#中的泛型
现在我们知道了.NET为什么要提供泛型,现在让我们学习什么是泛型和怎样实现吧!在C# 2.0中提供了泛型这一机制,但这可不是什么新奇的技术,因为早在C++和Java都提供泛型机制。
C#中提供五种泛型分别是:classes, structs, interfaces, delegates, and methods。
- 泛型类
也许有人说没有使用过C++的模板或Java的泛型,而且不了解C#中的泛型,真的是这样吗?其实我们经常都在使用泛型,相信很多人都使用过Dictionary<TKey,TValue>,没错这就是C#中提供的泛型字典,接下来我们将介绍自定义泛型类CustomStack。
/// <summary> /// A custom generic stack. /// </summary> public class CustomStack<T> { private int _stackPointer = 0; private T[] _stackArray; public void Push(T item) { ////Concrete implemention. } public T Pop() { ////Concrete implemention. } }
前面给出了泛型类CustomStack实现,我们发现只需在类名后添加<T>,并且把具体数据类型改为抽象的泛型类型T,现在我们就定义了一个简单泛型类,我们可以通过以下对比一下非泛型和泛型实现的区别。
图3非泛型和泛型类定义
通过上图我们发现泛型类CustomStack和非泛型类CustomStack在实现上没有太大的区别,我们只需把具体类型换成T就OK了,接下来我们将进入泛型的进阶学习。
- 泛型委托
关于委托大家可以阅读《.NET 中的委托》和《.NET中的委托和事件(续)》,现在让我们学习一下泛型委托。
图4泛型委托的定义
上图给出了泛型委托的定义,我们要注意Type parameters包括泛型参数和返回类型。随着C#3.0 Linq特性的引用,泛型委托的使用得到了极大地扩展,让我们通过具体代码讲讲如何使用泛型委托。
首先我们定义一个泛型委托,然后再定义一个Calc类,接着在Calc里定义两个方法分别是Add() 和Divide() 如下:
/// <summary> /// Define generic delegate. /// </summary> public delegate TR CalcMethod<T1, T2, TR>(T1 x, T2 y); public class Calc { static private double sum; /// <summary> /// Gets or sets the sum. /// </summary> /// <value> /// The sum. /// </value> static public double Sum { get { return sum; } set { sum = value; } } /// <summary> /// Adds the specified x. /// </summary> /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns></returns> static public string Add(int x , int y) { Sum = x + y; return Sum.ToString(); } /// <summary> /// Divides the specified x. /// </summary> /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns></returns> static public string Divide(int x, int y) { if (y == 0) { return "除数不能为零"; } Sum = x * 1.0 / y; return Sum.ToString(); } }
接下来我们需要定义一个委托变量,而后将委托变量和具体调用方法绑定就OK了。
////Initialize engeric delegate instance to Add method. CalcMethod<int, int, string> calcMethod = new CalcMethod<int, int, string>(Calc.Add);
- 类型约束
前面例子我们并没有对泛型类型进行限制,由于T是一个抽象类型,有时我们必须限制T是哪种类型,例如我们可以定义CustomStack<int>,CustomStack<double>或CustomStack<string>类型的栈,但有时我们必须限制泛型类型。这时我们需要使用类型约束,引入关键字----where。
C#中提供五种类型约束(类名,接口名,引用类型,值类型和构造函数),我们可以通过使用不同的类型约束来限定泛型类型。
类型约束 |
约束描述 |
ClassName |
T只能是该类或继承该类的子类 |
InterfaceName |
T只能是该接口或实行该接口的类型 |
struct |
T是任意值类型 |
class |
T是任意引用类型(如classes, arrays, delegates, and interfaces等) |
new() |
T是包含公有无参构成函数的任意类型 |
表1类型约束
通过表1我们初步地了解不同类型约束之间的区别,现在让我们通过具体的例子来看看它们使用范围和区别。
假设我们定义了一个泛型结构体struct RefSample<T> where T : class,现在考考大家以下例子符合类约束的有(提示值类型和引用类型区别):
A. RefSample<IDisposable>
B. RefSample<string>
C. RefSample<int[]>
D. RefSample<Guid>
E. RefSample<int>
激动人心的时刻又到了现在让我们快快公布答案吧!答案是ABC,因为该泛型的约束是引用类型约束,如果大家还不清楚请看一下引用类型约束的定义。
通过类约束例子,我们举一反三值类型约束就是约束类型为值类型。
OK接下来让我们介绍构造函数约束。在介绍构造函数约束之前,让我们回顾一下符合构造函数约束的条件:任意值类型,任意非静态非抽象无明确定义构造函数的类和任意非抽象有明确定义无参构造函数的类。这句话很别扭,让我们通过具体例子讲解一下。
//// Define generic method //// Which has constructor type constraints public T CreateInstance<T>() where T : new() { return new T(); } CreateInstance<int>(); CreateInstance<object>(); CreateInstance<string>();
现在我们定义了一个泛型方法并且添加构造函数约束,调用CreateInstance<int>() 和 CreateInstance<object>() 成功,但调用CreateInstance<string>() 失败,这由于String类没有提供无参构造函数。也许有人会问:“我们很难判断每种类型是否包含默认构无参造函数”,确确是这样但我们可以记住的是C#规定值类型提供默认无参构造函数。
- 组合约束
前面的泛型例子都是只使用一种类型约束,但有时候我们要使用多种类型约束,这时我们就可以使用组合约束,在使用约束的时我们要注意约束的先后次序,约束次序如下:
图5约束次序
通过上图我们发现ClassName,class和struct是主要约束,接口名是第二约束,最后是构造函数约束。OK现在我们对组合约束有了初步地了解,那么让我们通过以下例子加深理解。
A. class Sample<T> where T : class, struct
B. class Sample<T> where T : Stream, class
C. class Sample<T> where T : new(), Stream
D. class Sample<T> where T : IDisposable, Stream
E. class Sample<T> where T : XmlReader, IComparable, IComparable
F. class Sample<T,U> where T : struct where U : class, T
G. class Sample<T,U> where T : Stream, U : IDisposable
上面给出错误使用组合约束的例子(注:A~E单类型组合约束,FG多类型组合约束),请大家指出约束错误的原因,如果不清楚错误的原因大家回忆一下约束定义。
1.1.3 总结
本文注意介绍了C# 中泛型的基础知识给出自己的总结,而且这不是C# 泛型的全部,如果大家在看完本文之后希望进一步学习泛型这才是我的目的。
泛型的优点:
- 编译时进行类型检测,减少运行时异常InvalidCastException。
- 泛型使用强数据类型。如我们实例一个CustomStack<CustomStruct>对象objStack,通过调用Push()方法成功把CustomStruct压入栈中,而objStack.Push(“Stack”)失败。
- 值类型无需进行boxing和unboxing操作。例如前面的泛型Push()和Pop()方法存放和取出值类型时无需进行boxing和unboxing操作。
- 减少代码量。如:我们定义一个泛型栈,就不用像前面例子那样分别定义int和string类型的栈。
- 程序性能提高,因为无需类型转换,从而减少类型检测。
- 使用泛型减少内存消耗。因为无需进行boxing操作,不用重新分配堆空间。
- 代码可读性更强。
很多人喜欢将C# ,C++ 和Java中的泛型进行对比,而且也有很多相关的讨论,在这里我也给出自己的想法,我觉得C++ 的泛型功能的确比C# 和Java都要强大。