问题:
你需要理解.NET 类型如何适用于泛型,以及泛型.NET 类型与常规.NET 类型有着怎样的区别。
解决方案:
可以用两个快速的试验来展示常规.NET 类型和泛型.NET 类型之间的区别。在深入代码之前,如果你对泛型不熟悉,可以先跳到具体解释泛型的1.10.3 节,之后再回到本部分。
当定义一个常规.NET 类型时,它看起来就像是例1-6 中定义的FixedSizeCollection 类型。
例1-6:FixedSizeCollection(一种常规.NET 类型)
public class FixedSizeCollection
{
/// <summary>
/// 构造函数,增加静态计数器的值
/// 设置数据项***数量
/// </summary>
/// <param name="maxItems"></param>
public FixedSizeCollection(int maxItems)
{
FixedSizeCollection.InstanceCount++;
this.Items = new object[maxItems];
}
/// <summary>
/// 将一个未知类型的数据项添加到类中
/// object可以包含任何类型
/// </summary>
/// <param name="item">要添加的数据项</param>
/// <returns>新添加的数据项的索引</returns>
public int AddItem(object item)
{
if (this.ItemCount < this.Items.Length)
{
this.Items[this.ItemCount] = item;
return this.ItemCount++;
}
else
throw new Exception("Item queue is full");
}
/// <summary>
/// 从类中获得一个数据项
/// </summary>
/// <param name="index">要获取的数据项的索引</param>
/// <returns>object类型的一个数据项</returns>
public object GetItem(int index)
{
if (index >= this.Items.Length &&
index >= 0)
throw new ArgumentOutOfRangeException(nameof(index));
return this.Items[index];
}
#region Properties
/// <summary>
/// 静态的实例计数器,用于标准类型
/// </summary>
public static int InstanceCount { get; set; }
/// <summary>
/// 类中包含的数据项数量
/// </summary>
public int ItemCount { get; private set; }
/// <summary>
/// 类中包含的数据项
/// </summary>
private object[] Items { get; set; }
#endregion // Properties
/// <summary>
/// 重写ToString以提供类的详细信息
/// </summary>
/// <returns>包含类详细信息、格式化过的字符串</returns>
public override string ToString() =>
$"There are {FixedSizeCollection.InstanceCount.ToString()}
instances of {this.GetType().ToString()}
and this instance
contains {this.ItemCount}
items...";
}
FixedSizeCollection 拥有一个静态整型属性变量InstanceCount,在实例构造函数中递增;还包含一个ToString() 替代,用于输出这个AppDomain.FixedSizeCollection 中存在多少个FixedSizeCollection 的实例。此外,此集合类还包含一个objects(Items) 的数组,其大小由传入构造函数的项数量确定。FixedSizeCollection 还实现了两个方法(AddItem 和GetItem),用于添加和获取数据项;实现了一个只读属性(ItemCount),用于获取数组中当前的数据项数量。
FixedSizeCollection<T> 类型是一种泛型.NET 类型, 它具有相同的静态属性字段InstanceCount、统计实例化次数的实例构造函数,以及重写的ToString() 方法,用于指出这种类型的实例有多少个。FixedSizeCollection<T> 还具有一个Items 数组属性,以及与FixedSizeCollection 类对应的方法,如例1-7 所示。
例1-7:FixedSizeCollection<T>(一种泛型.NET 类型)
/// <summary>
/// 演示实例计数的泛型类
/// </summary>
/// <typeparam name="T">用于数组存储的类型参数</typeparam>
public class FixedSizeCollection<T>
{
/// <summary>
/// 构造函数,增加静态计数器,设置内部存储
/// </summary>
/// <param name="items"></param>
public FixedSizeCollection(int items)
{
FixedSizeCollection<T>.InstanceCount++;
this.Items = new T[items];
}
/// <summary>
/// 将一个数据项添加到类中,该数据项的类型由实例化的类型参数确定
/// </summary>
/// <param name="item">要添加的数据项</param>
/// <returns>新添加的数据项的从0开始的索引</returns>
public int AddItem(T item)
{
if (this.ItemCount < this.Items.Length)
{
this.Items[this.ItemCount] = item;
return this.ItemCount++;
}
else
throw new Exception("Item queue is full");
}
/// <summary>
/// 从类中获得一个数据项
/// </summary>
/// <param name="index">要获取的数据项的从0开始的索引</param>
/// <returns>实例化的类型的一个数据项</returns>
public T GetItem(int index)
{
if (index >= this.Items.Length &&
index >= 0)
throw new ArgumentOutOfRangeException(nameof(index));
return this.Items[index];
}
#region Properties
/// <summary>
/// 静态的实例计数器,用于泛型类实例化的类型
/// </summary>
public static int InstanceCount { get; set; }
/// <summary>
/// 类中包含的数据项的数量
/// </summary>
public int ItemCount { get; private set; }
/// <summary>
/// 类中包含的数据项
/// </summary>
private T[] Items { get; set; }
#endregion // Properties
/// <summary>
/// 重写ToString以提供类的详细信息
/// </summary>
/// <returns>包含类详细信息、格式化过的字符串</returns>
public override string ToString() =>
$"There are {FixedSizeCollection<T>.InstanceCount.ToString()}
instances of {this.GetType().ToString()}
and this instance
contains {this.ItemCount}
items...";
}
当你查看Items 数组属性的实现时,FixedSizeCollection<T> 开始变得有些不同了。Items数组声明如下:
private T[] Items { get; set; }
而不是:
private object[] Items { get; set; }
Items 数组属性使用泛型类的类型参数(<T>) 来确定允许哪些类型的数据项。
FixedSizeCollection 用object 作为Items 数组属性的类型,它允许把任何类型存储在数据项的数组中(因为所有类型都可转换为object);而 FixedSizeCollection<T> 通过类型参数确定允许哪些类型的对象,从而提供了类型安全。另外要注意的是,这些属性没有声明相关联的私有字段以存储数组。这个示例使用了C# 3.0 中新增的自动实现的属性。在底层,C# 编译器为属性对应的类型创建了一个存储元素,但是只要你不需要在访问属性时执行特定的代码,就不再需要为此属性存储编写代码。要使属性只读,只要将set; 声明标记为private 即可。
在AddItem 和GetItem 的方法声明中可以看出另一个区别。AddItem 现在需要类型为T 的一个参数,而在FixedSizeCollection 中,它需要一个object 类型的参数。GetItem 现在返回一个类型为T 的值,而在FixedSizeCollection 中,它返回一个object 类型的值。这些改变允许FixedSizeCollection<T> 中的方法使用实例化的类型存储和获取数组中的数据项,而不必像在FixedSizeCollection 中那样允许存储任何object。
/// <summary>
/// 将一个数据项添加到类中,该数据项的类型由实例化的类型参数确定
/// </summary>
/// <param name="item">要添加的数据项</param>
/// <returns>新添加的数据项的从0开始的索引</returns>
public int AddItem(T item)
{
if (this.ItemCount < this.Items.Length)
{
this.Items[this.ItemCount] = item;
return this.ItemCount++;
}
else
throw new Exception("Item queue is full");
}
/// <summary>
/// 从类中获得一个数据项
/// </summary>
/// <param name="index">要获取的数据项的从0开始的索引</param>
/// <returns>实例化的类型的一个数据项</returns>
public T GetItem(int index)
{
if (index >= this.Items.Length &&
index >= 0)
throw new ArgumentOutOfRangeException("index");
return this.Items[index];
}
这提供了几个优势。首先最重要的是,FixedSizeCollection<T> 为数组中的数据项提供的类型安全。可以在FixedSizeCollection 中编写如下代码:
// 常规类
FixedSizeCollection C = new FixedSizeCollection(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);
// 将一个int添加到string数组中,完全没问题
C.AddItem(i1);
但是如果你尝试执行相同的操作,FixedSizeCollection<T> 将返回一个错误给编译器。
// 泛型类
FixedSizeCollection<string> gC = new FixedSizeCollection<string>(5);
Console.WriteLine(gC);
string s1 = "s1";
string s2 = "s2";
string s3 = "s3";
int i1 = 1;
// 添加到泛型类(作为string)
gC.AddItem(s1);
gC.AddItem(s2);
gC.AddItem(s3);
// 试图将一个int添加到string实例,被编译器拒绝
// error CS1503: Argument '1': cannot convert from 'int' to 'string'
//gC.AddItem(i1);
由编译器阻止它在运行时导致错误是个非常好的想法。
也许你并不会立刻注意到,但是在FixedSizeCollection 中把整数添加到object 数组中时,实际会对整数进行装箱。在FixedSizeCollection 上调用GetItem 的IL 中可以看出这一点。
IL_0177: ldloc.2
IL_0178: ldloc.s i1
IL_017a: box [mscorlib]System.Int32
IL_017f: callvirt instance int32
CSharpRecipes.ClassesAndGenerics/FixedSizeCollection::AddItem(object)
这一装箱操作把值类型的int 转变成引用类型(object),以便存储在数组中。这导致在object 数组中保存值类型时需要额外的工作。
在FixedSizeCollection 的实现中从类取回一个数据项时会遇到另一个问题。看一下FixedSizeCollection.GetItem 如何获取一个数据项。
// 保存获取的string
string sHolder;
// 需要进行转换,否则会出现错误CS0266:
// 无法隐式地将类型object转换为string
sHolder = (string)C.GetItem(1);
由于FixedSizeCollection.GetItem 返回的数据项是object 类型,需要将其强制转换成string,以便获得你所希望的用于索引1 位置的string。它可能并不是一个string,你只能确定它是一个object,但是必须将它强制转换成一种更具体的类型,才可以正确地给它赋值。
FixedSizeCollection<T> 的实现修正了这些问题。与FixedSizeCollection 不同,FixedSize-Collection<T> 中并不需要拆箱操作,因为GetItem 的返回类型是实例化的类型,编译器通过检查将要返回的值确保了这一点。
// 保存获取的string
string sHolder;
int iHolder;
// 不需要类型转换
sHolder = gC.GetItem(1);
// 试图将一个string保存到int变量
// 错误CS0029:无法隐式地将类型'string'转换为'int'
//iHolder = gC.GetItem(1);
为了看出两种类型之间的另一个区别,分别实例化每种类型的几个实例,代码如下所示。
// 常规类
FixedSizeCollection A = new FixedSizeCollection(5);
Console.WriteLine(A);
FixedSizeCollection B = new FixedSizeCollection(5);
Console.WriteLine(B);
FixedSizeCollection C = new FixedSizeCollection(5);
Console.WriteLine(C);
// 泛型类
FixedSizeCollection<bool> gA = new FixedSizeCollection<bool>(5);
Console.WriteLine(gA);
FixedSizeCollection<int> gB = new FixedSizeCollection<int>(5);
Console.WriteLine(gB);
FixedSizeCollection<string> gC = new FixedSizeCollection<string>(5);
Console.WriteLine(gC);
FixedSizeCollection<string> gD = new FixedSizeCollection<string>(5);
Console.WriteLine(gD);
上述代码的输出结果如下所示。
There are 1 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection
and this instance contains 0 items...
There are 2 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection
and this instance contains 0 items...
There are 3 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection
and this instance contains 0 items...
There are 1 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection'1
[System.Boolean] and this instance contains 0 items...
There are 1 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection'1
[System.Int32] and this instance contains 0 items...
There are 1 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection'1
[System.String] and this instance contains 0 items...
There are 2 instances of CSharpRecipes.ClassesAndGenerics+FixedSizeCollection'1
[System.String] and this instance contains 0 items...
讨论:
即使你不知道将要处理的最终类型,泛型中的类型参数也能让你创建类型安全的代码。在许多实例中,你希望类型具有某些特征,在这种情况下可以对类型施加一些限制(参见范例1.12,即1.12 节)。方法可以具有泛型类型参数,而不管类自身是否具有它们。
注意常规类FixedSizeCollection 具有三个实例,而泛型类FixedSizeCollection<T> 有一个声明为bool 类型的实例,一个声明为int 的实例,两个声明为string 类型的实例。这意味着每个非泛型类对应创建了一个.NET Type 对象,而一个泛型类的每个类型实例化都创建了一个.NET Type 对象。
在示例代码中,FixedSizeCollection 具有三个实例,因为FixedSizeCollection 只有一个由CLR 维护的类型。对于泛型,每个类模板与类型实例构造时传入的类型参数的组合都维护了一个类型。换句话说,你得到了一个用于FixedSizeCollection<bool> 的.NET 类型,一个用于FixedSizeCollection<int> 的.NET 类型,还有一个用于FixedSizeCollection<string>的.NET 类型。
静态属性InstanceCount 有助于阐释这一点,因为类的静态属性实际上与CLR 维护的类型相关联。CLR 只会对任何给定的类型创建一次,然后维护它,直到应用程序域卸载。这也就是发生以下情况的原因:对这些对象调用ToString() 的结果显示 FixedSizeCollection的计数是3(因为确实只有其中1 个计数器),而FixedSizeCollection<T> 类型的计数器是1 或2。
参考:
MSDN 文档中的“泛型类型参数”和“泛型类”主题。