除了System.Collections.Generic命名空间之外,.NET Framework还有其他泛型类型。这里讨论的结构和委托都位于System命名空间中,用于不同的目的。
本节讨论如下内容:
结构Nullable<T>
委托EventHandler<TEventArgs>
结构ArraySegment<T>
数据库中的数字和编程语言中的数字有显著不同的特征,因为数据库中的数字可以为空,C#中的数字不能为空。Int32是一个结构,而结构实现为值类型,所以它不能为空。
只有在数据库中,而且把XML数据映射为.NET类型,才不存在这个问题。
这种区别常常令人很头痛,映射数据也要多做许多工作。一种解决方案是把数据库和XML文件中的数字映射为引用类型,因为引用类型可以为空值。但这也会在运行期间带来额外的系统开销。
使用Nullable<T>结构很容易解决这个问题。在下面的例子中,Nullable<T>用Nullable<int>实例化。变量x现在可以像int那样使用了,进行赋值或使用运算符执行一些计算。这是因为我们转换了Nullable<T>类型的运算符。x还可以是空。可以检查Nullable<T>的HasValue和Value属性,如果该属性有一个值,就可以访问该值:
Nullable<int> x;
x = 4;
x += 3;
if (x.HasValue)
{
int y = x.Value;
}
x = null;
因为可空类型使用得非常频繁,所以C#有一种特殊的语法,用于定义这种类型的变量。定义这类变量时,不使用一般结构的语法,而使用?运算符。在下面的例子中,x1和x2都是可空int类型的实例:
Nullable<int> x1;
int? x2;
可空类型可以与null和数字比较,如上所示。这里,x的值与null比较,如果x不是null,就与小于0的值比较:
int? x = GetNullableType(); Please use brackets here. Good coding standards. [CN]
can do it here
if (x == null)
{
Console.WriteLine("x is null");
}
else if (x < 0)
{
Console.WriteLine("x is smaller than 0");
}
可空类型还可以使用算术运算符。变量x3是变量x1和x2的和。如果这两个可空变量中有一个的值是null,它们的和就是null。
int? x1 = GetNullableType();
int? x2 = GetNullableType();
int? x3 = x1 + x2;
非可空类型可以转换为可空类型。从非可空类型转换为可空类型时,在不需要强制类型转换的地方可以进行隐式转换。这种转换总是成功的:
int y1 = 4;
int? x1 = y1;
但从可空类型转换为非可空类型可能会失败。如果可空类型的值是null,把null值赋予非可空类型,就会抛出InvalidOperationException类型的异常。这就是进行显式转换时需要类型转换运算符的原因:
int? x1 = GetNullableType();
int y1 = (int)x1;
如果不进行显式类型转换,还可以使用接合运算符(coalescing operator)从可空类型转换为非可空类型。接合运算符的语法是??,为转换定义了一个默认值,以防可空类型的值是null。这里,如果x1是null,y1的值就是0。
int? x1 = GetNullableType();
int y1 = x1 ?? 0;
在Windows Forms和Web应用程序中,为许多不同的事件处理程序定义了委托。其中一些事件处理程序如下:
public sealed delegate void EventHandler(object sender, EventArgs e);
public sealed delegate void PaintEventHandler(object sender, PaintEventArgs e);
public sealed delegate void MouseEventHandler(object sender, MouseEventArgs e);
这些委托的共同点是,第一个参数总是sender,它是事件的起源,第二个参数是包含事件特定信息的类型。
使用新的EventHandler<TEventArgs>,就不需要为每个事件处理程序定义新委托了。可以看出,第一个参数的定义方式与以前一样,但第二个参数是一个泛型类型TeventArgs。where子句指定TEventArgs的类型必须派生于基类EventArgs。
public sealed delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs : EventArgs
结构ArraySegment<T>表示数组的一段。如果需要数组的一部分,就可以使用数组段。在ArraySegment<T>中,包含了数组段的信息(偏移量和元素个数)。
在下面的例子中,变量arr定义为有8个元素的int数组。ArraySegment<int>类型的变量segment用于表示该整数数组的一段。该段用构造函数初始化,在这个构造函数中,传送了该数组、偏移量和元素个数。其中偏移量设置为2,所以从第三个元素开始,元素个数设置为3,所以6是数组段的最后一个元素。
数组段可以用Array属性访问。ArraySegment<T>还有Offset和Count属性,表示定义数组段的初始化了的值。for循环用于迭代数组段。for循环的第一个表达式初始化为迭代开始的偏移量。第二个表达式指定数组段中的元素个数,以确定迭代是否停止。在for循环中,数组段包含的元素用Array属性来访问:
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
ArraySegment<int> segment = new ArraySegment<int>(arr, 2, 3);
for (int i = segment.Offset; i < segment.Offset + segment.Count+1; i++)
{
Console.WriteLine(segment.Array[i]);
}
在上面的例子中,ArraySegment<T>结构有什么用处?ArraySegment<T>可以作为参数传送给方法。这样,只要一个参数就可以定义数组、偏移量和元素个数,而不是3个参数。
WorkWithSegment()方法把ArraySegment<string>作为参数。在这个方法的实现代码中,Offset、Count和Array属性的用法与以前相同:
void WorkWithSegment(ArraySegment<string> segment)
{
for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)
{
Console.WriteLine(segment.Array[i]);
}
}
注意:
数组段不复制原数组的元素,但原数组可以通过ArraySegment<T>访问。如果数组段中的元素改变了,这些变化也会反映到原数组中。