从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类型。
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];
}
这样做的优势在于,首先通过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个实例。