泛型并不是一个全新的结构,其他语言中有类似的概念。例如,C++模板就与泛型相当。但是,C++模板和.NET泛型之间有一个很大的区别。对于C++模板,在用特定的类型实例化模板时,需要模板的源代码。相反,泛型不仅是C#语言的一种结构,而且是CLR定义的。所以,即使泛型类是在C#中定义的,也可以在Visual Basic中用一个特定的类型实例化该泛型。
值类型存储在堆栈上,引用类型存储在堆上。C#类是引用类型,结构是值类型。.NET很容易把值类型转换为引用类型,所以可以在需要对象(对象是引用类型)的任意地方使用值类型。例如,int可以赋予一个对象。从值类型转换为引用类型称为装箱。如果方法需要把一个对象作为参数,而且传送了一个值类型,装箱操作就会自动进行。另一方面,装箱的值类型可以使用拆箱操作转换为值类型。在拆箱时,需要使用类型转换运算符。
下面的例子显示了System.Collections命名空间中的ArrayList类。ArrayList存储对象, Add()方法定义为需要把一个对象作为参数,所以要装箱一个整数类型。在读取ArrayList中的值时,要进行拆箱,把对象转换为整数类型。可以使用类型转换运算符把ArrayList集合的第一个元素赋予变量i1,在访问int类型的变量i2的foreach语句中,也要使用类型转换运算符:
ArrayList list = new ArrayList();
list.Add(44); // boxing – convert a value type to a reference type
int i1 = (int)list[0]; // unboxing – convert a reference type to a value type
foreach (int i2 in list)
{
Console.WriteLine(i2); // unboxing
}
装箱和拆箱操作很容易使用,但性能损失比较大,迭代许多项时尤其如此。
System.Collections.Generic命名空间中的List<T>类不使用对象,而是在使用时定义类型。在下面的例子中,List<T>类的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进行装箱和拆箱操作:
List<int> list = new List<int>();
list.Add(44); // no boxing – value types are stored in the List<int>
int i1 = list[0]; // no unboxing, no cast needed
foreach (int i2 in list)
{
Console.WriteLine(i2);
}
9.1.2 类型安全
泛型的另一个特性是类型安全。与ArrayList类一样,如果使用对象,可以在这个集合中添加任意类型。下面的例子在ArrayList类型的集合中添加一个整数、一个字符串和一个MyClass类型的对象:
ArrayList list = new ArrayList();
list.Add(44);
list.Add("mystring");
list.Add(new MyClass());
如果这个集合使用下面的foreach语句迭代,而该foreach语句使用整数元素来迭代,编译器就会编译这段代码。但并不是集合中的所有元素都可以转换为int,所以会出现一个运行异常:
foreach (int i in list)
{
Console.WriteLine(i);
}
错误应尽早发现。在泛型类List<T>中,泛型类型T定义了允许使用的类型。有了List<int>的定义,就只能把整数类型添加到集合中。编译器不会编译这段代码,因为Add()方法的参数无效:
List<int> list = new List<int>();
list.Add(44);
list.Add("mystring"); // compile time error
list.Add(new MyClass()); // compile time error
9.1.3 二进制代码的重用
泛型允许更好地重用二进制代码。泛型类可以定义一次,用许多不同的类型实例化。不需要像C++模板那样访问源代码。
例如,System.Collections.Generic命名空间中的List<T>类用一个int、一个字符串和一个MyClass类型实例化:
List<int> list = new List<int>();
list.Add(44);
List<string> stringList = new List<string>();
stringList.Add("mystring");
List<MyClass> myclassList = new List<MyClass>();
myClassList.Add(new MyClass());
泛型类型可以在一种语言中定义,在另一种.NET语言中使用。
如果在程序中使用泛型,区分泛型类型和非泛型类型会有一定的帮助。下面是泛型类型的命名规则:
● 泛型类型的名称用字母T作为前缀。
● 如果没有特殊的要求,泛型类型允许用任意类替代,且只使用了一个泛型类型,就可以用字符T作为泛型类型的名称。
public class List<T> { }
public class LinkedList<T> { }
● 如果泛型类型有特定的要求(例如必须实现一个接口或派生于基类),或者使用了两个或多个泛型类型,就应给泛型类型使用描述性的名称:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public delegate TOutput Converter<TInput, TOutput>(TInput from);
public class SortedList<TKey, TValue> { }
9.2 创建泛型类
首先介绍一个一般的、非泛型的简化链表类,它可以包含任意类型的对象,以后再把这个类转化为泛型类。
在链表中,一个元素引用其后的下一个元素。所以必须创建一个类,将对象封装在链表中,引用下一个对象。类LinkedListNode包含一个对象value,它用构造函数初始化,还可以用Value属性读取。另外,LinkedListNode类包含对链表中下一个元素和上一个元素的引用,这些元素都可以从属性中访问。
public class LinkedListNode
{
private object value;
public LinkedListNode(object value)
{
this.value = value;
}
public object Value
{
get { return value; }
}
private LinkedListNode next;
public LinkedListNode Next
{
get { return next; }
internal set { next = value; }
}
private LinkedListNode prev;
public LinkedListNode Prev
{
get { return prev; }
internal set { prev = value; }
}
}
LinkedList类包含LinkedListNode类型的first和last字段,它们分别标记了链表的头尾。AddLast()方法在链表尾添加一个新元素。首先创建一个LinkedListNode类型的对象。如果链表是空的,则first和last字段就设置为该新元素;否则,就把新元素添加为链表中的最后一个元素。执行GetEnumerator()方法时,可以用foreach语句迭代链表。GetEnumerator()方法使用yield语句创建一个枚举器类型。
提示:
yield语句参见第5章。
public class LinkedList : IEnumerable
{
private LinkedListNode first;
public LinkedListNode First
{
get { return first; }
}
private LinkedListNode last;
public LinkedListNode Last
{
get { return last; }
}
public LinkedListNode AddLast(object node)
{
LinkedListNode newNode = new LinkedListNode(node);
if (first == null)
{
first = newNode;
last = first;
}
else
{
last.Next = newNode;
last = newNode;
}
return newNode;
}
public IEnumerator GetEnumerator()
{
LinkedListNode current = first;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
}
现在可以给任意类型使用LinkedList类了。在下面的代码中,实例化了一个新LinkedList对象,添加了两个整数类型和一个字符串类型。整数类型要转换为一个对象,所以执行装箱操作,如前面所述。在foreach语句中执行拆箱操作。在foreach语句中,链表中的元素被强制转换为整数,所以对于链表中的第三个元素,会发生一个运行异常,因为它转换为int时会失败。
LinkedList list1 = new LinkedList();
list1.AddLast(2);
list1.AddLast(4);
list1.AddLast("6");
foreach (int i in list1)
{
Console.WriteLine(i);
}
下面创建链表的泛型版本。泛型类的定义与一般类类似,只是要使用泛型类型声明。之后,泛型类型就可以在类中用作一个字段成员,或者方法的参数类型。LinkedListNode类用一个泛型类型T声明。字段value的类型是T,而不是object。构造函数和Value属性也变为接受和返回T类型的对象。也可以返回和设置泛型类型,所以属性Next和Prev的类型是LinkedListNode<T>。
public class LinkedListNode<T>
{
private T value;
public LinkedListNode(T value)
{
this.value = value;
}
public T Value
{
get { return value; }
}
private LinkedListNode<T> next;
public LinkedListNode<T> Next
{
get { return next; }
internal set { next = value; }
}
private LinkedListNode<T> prev;
public LinkedListNode<T> Prev
{
get { return prev; }
internal set { prev = value; }
}
}
下面的代码把LinkedList类也改为泛型类。LinkedList<T>包含LinkedListNode<T>元素。LinkedList中的类型T定义了类型T的包含字段first和last。AddLast()方法现在接受类型T的参数,实例化LinkedListNode<T>类型的对象。
IEnumerable接口也有一个泛型版本IEnumerable<T>。IEnumerable<T>派生于IEnumerable,添加了返回IEnumerator<T>的GetEnumerator()方法,LinkedList<T>执行泛型接口IEnumerable<T>。
提示:
枚举、接口IEnumerable和IEnumerator详见第5章。
public class LinkedList<T> : IEnumerable<T>
{
private LinkedListNode<T> first;
public LinkedListNode<T> First
{
get { return first; }
}
private LinkedListNode<T> last;
public LinkedListNode<T> Last
{
get { return last; }
}
public LinkedListNode<T> AddLast(T node)
{
LinkedListNode<T> newNode = new LinkedListNode<T>(node);
if (first == null)
{
first = newNode;
last = first;
}
else
{
last.Next = newNode;
last = newNode;
}
return newNode;
}
public IEnumerator<T> GetEnumerator()
{
LinkedListNode<T> current = first;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
使用泛型类LinkedList<T>,可以用int类型实例化它,且无需装箱操作。如果不使用AddLast()方法传送int,就会出现一个编译错误。使用泛型IEnumerable<T>,foreach语句也是类型安全的,如果foreach语句中的变量不是int,也会出现一个编译错误。
LinkedList<int> list2 = new LinkedList<int>();
list2.AddLast(1);
list2.AddLast(3);
list2.AddLast(5);
foreach (int i in list2)
{
Console.WriteLine(i);
}
同样,可以给泛型LinkedList<T>使用string类型,将字符串传送给AddLast()方法。
LinkedList<string> list3 = new LinkedList<string>();
list3.AddLast("2");
list3.AddLast("four");
list3.AddLast("foo");
foreach (string s in list3)
{
Console.WriteLine(s);
}
提示:
每个处理对象类型的类都可以有泛型实现方式。另外,如果类使用了继承,泛型非常有助于去除类型转换操作。