C#泛型秘诀(1)
本系列文章翻译O'Reilly 出版的《C# Cookbook》一书中的片段,仅供学习交流使用
4.0 介绍
泛型,一个期待已久的功能,随着C# 2.0版本编译器的到来最终出现。泛型是一个非常有用的功能,它使得您的代码变得精简而富有效率。这些将在秘诀4.1进行详细讲述。泛型的到来使得您可以编写更为强大的应用程序,但这需要正确地使用它。如果您考虑把ArrayList,Queue,Stack和Hashtable对象转变为使用相应的泛型版本,可以阅读秘诀4.4,4.5和4.10。当您阅读过后,会发现这种转变不一定简单,甚至有可能会不再打算进行转变。
本章的另外一些秘诀涉及到.NET Framework 2.0所包含的其他泛型类,如秘诀4.6。其他秘诀讲述一些泛型类的操作,如秘诀4.2,4.8和4.13。
4.1决定在何时何地使用泛型
问题
您希望在一个新工程内使用泛型,或者想把已有项目中的非泛型类转换为等价的泛型版本。但您并非了解为何要这样做,也不知道哪个非泛型类应该被转换为泛型类。
解决方案
决定在何时何地使用泛型,您需要考虑以下几件事件:
l 您所使用的类型是否包含或操作未指定的数据类型(如集合类型)?如果是这样,如果是这样,创建泛型类型将能提供更多的好处。如果您的类型只操作单一的指定类型,那么就没有必要去创建一个泛型类。
l 如果您的类型将操作值类型,那么就会产生装箱和拆箱操作,就应该考虑使用泛型来防止装箱和拆箱操作。
l 泛型的强类型检查有助于快速查找错误(也就是编译期而非运行期),从而缩短bug修复周期。
l 在编写多个类操作多个数据类型时是否遭遇到“代码膨胀”问题(如一个ArrayList只存储StreamReaders而另一个存储StreamWriters)?其实编写一次代码并让它工作于多个数据类型非常简单。
l 泛型使得代码更为清晰。通过消除代码膨胀并进行强制检查,您的代码将变得更易于阅读和理解。
讨论
很多时候,使用泛型类型将使您受益。泛型将使得代码重用更有效率,具有更快的执行速度,进行强制类型检查,获得更易读的代码。
阅读参考
MSDN文档中的“Generics Overview”和“Benefits of Generics”主题。
4.2 理解泛型类型
问题
您需要理解泛型类型在.NET中是如何工作的,它跟一般的.NET类型有什么不同。
解决方案
几个小实验就可以演示一般类型和泛型类型之间的区别。例4-1中的StandardClass类就是一个般类型。
例4-1 StandardClass:一般的.NET类型
{
static int _count = 0; //StandardClass类的对象的表态计数器
int _maxItemCount; //项数目的最大值
object[] _items; //保存项的数组
int _currentItem = 0; //当前项数目
public StandardClass(int items) //构造函数
{
_count++; //对象数目加
_maxItemCount = items;
_items = new object[_maxItemCount]; //数组初始化
}
//用于添加项,为了适用于任何类型,使用了object类型
public int AddItem(object item)
{
if (_currentItem < _maxItemCount)
{
_items[_currentItem] = item;
return _currentItem++; //返回添加的项的索引
}
else
throw new Exception("Item queue is full");
}
//用于从类中取得指定项
public object GetItem(int index)
{
Debug.Assert(index < _maxItemCount); //设置断言
if (index >= _maxItemCount)
throw new ArgumentOutOfRangeException("index");
return _items[index]; //返回指定项
}
public int ItemCount //属性,指示当前项数目
{
get { return _currentItem; }
}
public override string ToString( )
{ //重载ToString方法,用于介绍类的情况
return "There are " + _count.ToString( ) +
" instances of " + this.GetType( ).ToString( ) +
" which contains " + _currentItem + " items of type " +
_items.GetType( ).ToString( ) + "";
}
}
StandardClass类有一个整型静态成员变量_count,用于在实例构造器中计数。重载的ToString()方法打印在这个应用程序域中StandardClass类实例的数目。StandardClass类还包括一个object数组(_item),它的长度由构造方法中的传递的参数来决定。它实现了添加和获得项的方法(AddItem,GetItem),还有一个只读属性来获取数组中的项的数目(ItemCount)。
GenericClass<T>类是一个泛型类型,同样有静态成员变量_count,实例构造器中对实例数目进行计算,重载的ToString()方法告诉您有多少GenericClass<T>类的实例存在。GenericClass<T>也有一个_itmes数组和StandardClass类中的相应方法,请参考例4-2。
Example4-2 GenericClass<T>:泛型类
{
static int _count = 0;
int _maxItemCount;
T[] _items;
int _currentItem = 0;
public GenericClass(int items)
{
_count++;
_ _maxItemCount = items;
_items = new T[_maxItemCount];
}
public int AddItem(T item)
{
if (_currentItem < _maxItemCount)
{
_items[_currentItem] = item;
return _currentItem++;
}
else
throw new Exception("Item queue is full");
}
public T GetItem(int index)
{
Debug.Assert(index < _maxItemCount);
if (index >= _maxItemCount)
throw new ArgumentOutOfRangeException("index");
return _items[index];
}
public int ItemCount
{
get { return _currentItem; }
}
public override string ToString()
{
return "There are " + _count.ToString() +
" instances of " + this.GetType().ToString() +
" which contains " + _currentItem + " items of type " +
_items.GetType().ToString() + "";
}
}
从GenericClass<T>中的少许不同点开始,看看_items数组的声明。它声明为:
T[] _items;
而不是
object[] _items;
_items数组使用泛型类(<T>)做为类型参数以决定在_itmes数组中接收哪种类型的项。StandarClass在_itmes数组中使用Objcec以使得所有类型都可以做为项存储在数组中(因为所有类型都继承自object)。而GenericClass<T>通过使用类型参数指示允许使用的对象类型来提供类型安全。
下一个不同在于AddItem和GetItem方法的声明。AddItem现在使用一个类型T做为参数,而在StandardClass中使用object类型做为参数。GetItem现在的返回值类型T,StandardClass返回值为object类型。这个改变允许GenericClass<T>中的方法在数组中存储和获得具体的类型而非StandardClass中的允许存储所有的object类型。
{
if (_currentItem < _maxItemCount)
{
_items[_currentItem] = item;
return _currentItem++;
}
else
throw new Exception("Item queue is full");
}
public T GetItem(int index)
{
Debug.Assert(index < _maxItemCount);
if (index >= _maxItemCount)
throw new ArgumentOutOfRangeException("index");
return _items[index];
}
这样做的优势在于,首先通过GenericClass<T>为数组中的项提供了类型安全。在StandardClass中可能会这样写代码:
StandardClass C = new StandardClass(5);
Console.WriteLine(C);
string s1 = "s1";
string s2 = "s2";
string s3 = "s3";
int i1 = 1;
// 在一般类中以object的形式添加项
C.AddItem(s1);
C.AddItem(s2);
C.AddItem(s3);
// 在字符串数组中添加一个整数,也被允许
C.AddItem(i1);
但在GenericClass<T>中做同样的事情将导致编译错误:
// 泛型类
GenericClass<string> gC = new GenericClass<string>(5);
Console.WriteLine(gC);
string s1 = "s1";
string s2 = "s2";
string s3 = "s3";
int i1 = 1;
// 把字符串添加进泛型类.
gC.AddItem(s1);
gC.AddItem(s2);
gC.AddItem(s3);
// 尝试在字符串实例中添加整数,将被编译器拒绝
// error CS1503: Argument '1': cannot convert from 'int' to 'string'
//GC.AddItem(i1);
编译器防止它成为运行时源码的bug,这是一件非常美妙的事情。
虽然并非显而易见,但在StandardClass中把整数添加进object数组会导致装箱操作,这一点可以StandardClass调用GetItem方法时的IL代码:
IL_0170: ldloc.2
IL_0171: ldloc.s i1
IL_0173: box [mscorlib]System.Int32
IL_0178: callvirt instance int32 CSharpRecipes.Generics/StandardClass::AddItem(object)
这个装箱操作把做为值类型的整数转换为引用类型(object),从而可以在数组中存储。这导致了在object数组中存储值类型时需要增加额外的工作。
当您在运行StandardClass并从类中返回一个项时,还会产生一个问题,来看看StandardClass.GetItem如何返回一个项:
// 存储返回的字符串.
string sHolder;
// 发生错误CS0266:
// Cannot implicitly convert type 'object' to 'string'…
sHolder = (string)C.GetItem(1);
因为StandardClass.GetItem返回的是object类型,而您希望通过索引1获得一个字符串类型,所以需要把它转换为字符串类型。然而它有可能并非字符串-----只能确定它是一个object-----但为了赋值正确,您不得不把它转换为更明确的类型。字符串比较特殊,所有对象都可以自行提供一个字符串描述,但当数组接收一个double类型并把它赋给一个布尔类型就会出问题。
这两个问题在GenericClass<T>中被全部解决。无需再进行拆箱,因为GetItem所返回的是一个具体类型,编译器会检查返回值以强近它执行。
string sHolder;
int iHolder;
// 不需要再进行转换
sHolder = gC.GetItem(1);
// 尝试把字符串变为整数.将出现
// 错误CS0029: Cannot implicitly convert type 'string' to 'int'
//iHolder = gC.GetItem(1);
为了了解两种类型的其他不同点,分别给出它们的示例代码:
StandardClass A = new StandardClass();
Console.WriteLine(A);
StandardClass B = new StandardClass();
Console.WriteLine(B);
StandardClass C = new StandardClass();
Console.WriteLine(C);
// 泛型类
GenericClass<bool> gA = new GenericClass<bool>();
Console.WriteLine(gA);
GenericClass<int> gB = new GenericClass<int>();
Console.WriteLine(gB);
GenericClass<string> gC = new GenericClass<string>();
Console.WriteLine(gC);
GenericClass<string> gD = new GenericClass<string>();
Console.WriteLine(gD);
上述代码输出结果如下:
There are 1 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]...
There are 2 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]...
There are 3 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]...
There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.Boolean] which contains 0 items of type System.Boolean[]...
There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.Int32] which contains 0 items of type System.Int32[]...
There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.String] which contains 0 items of type System.String[]...
There are 2 instances of CSharpRecipes.Generics+GenericClass`1[System.String] which contains 0 items of type System.String[]...
讨论
泛型中的类型参数允许您在不知道使用何种类型的情况下提供类型安全的代码。在很多场合下,您希望类型具有某些指定的特征,这可以通过使用类型约束(秘诀4.12)来实现。方法在类本身不是泛型的情况下也可以拥有泛型类型的参数。秘诀4.9为此演示了一个例子。
注意当StandardClass拥有三个实例,GenericClass有一个声明为<bool>类型的实例,一个声明为<int>类型的实例,两个声明为<string>类型的实例。这意味着所有非泛型类都创建同一.NET类型对象,而所有泛型类都为指定类型实例创建自己的.NET类型对象。
示例代码中的StandardClass类有三个实例,因为CLR中只维护一个StandardClass类型。而在泛型中,每种类型都被相应的类型模板所维护,当创建一个类型实例时,类型实参被传入。说得更清楚一些就是为GenericClass<bool>产生一个类型,为GenericClass<int>产生一个类型,为GenericClass<string>产生第三个类型。
内部静态成员_count可以帮助说明这一点,一个类的静态成员实际上是跟CLR中的类型相连的。CLR对于给定的类型只会创建一次,并维护它一直到应用程序域卸载。这也是为什么在调用ToString方法时,输出显示有StandardClass的三个实例,而GenericClass<T>类型有1和2个实例。
阅读参考
MSDN文档中的“Generic Type Parameters”和“Generic Classes”主题。