摘要
在.net1.x中处理值类型(value-types)的空值是比较困难的。在.NET2.0中可以借助泛型和Nullable类型来克服这一缺点。这篇文章给你示范什么是Nullable类型以及如何使用。
使用Nullable类型的场合
假想一种情况在你的代码中要保存货币的数值。现在的软件是严格的数学规则驱动的,一般需要量化所使用的变量。这就是为什么在.NET中每种类型都有默认值的原因。值类型的变量总是有预定义的值,比如说整型(int)的默认值是0,布尔(bool)类型的默认值是false。到目前为止一切尚好,但是当我们在真实世界中(不一定有使用所有的数据变量)应用严格的数学规则的时候,问题出现了。举个例子,用0如何区别用户没有钱和用户没有输入任何可以在软件中存储的数据呢?当你使用关系型数据库的时候这种情况会更加明显。关系型数据有空值(null value)的概念,用来表示没有数据,但是对于值类型来说,在.NET编程语言中并没有提供合适的值。
使用当前的技术来解决Nullable值的问题
多少年来开发人员采用了好多种方法来解决这个问题,这里将会简单提到其中的一些。
使用特殊值
其中一个最明显的解决方案就是使用一些特殊的不用的值来代替空值。比如你要处理年龄,你定义了整型并且用-1来代替空值。这种解决方法只有在确定这个值是肯定不会用到而且不会和其中真实值产生混淆的时候是可行的。再比如如果你用-1代表你的美元数量,那可能表示你将失去一块钱,所以这种情况下使用-1来代替是不合适的。这种解决方案很容易实现,但是一般来说是不赞成的因为会造成潜在的运行时难以发现的bug,尤其是应用程序的需求在后期变化的时候,前面被当做空值使用的值将变成合法的数据。
使用标记
我曾经经常用的一种方案是给包含空值的对象定义一个标记来区别是否是空值。顺便提一下系统的类也是使用这种方法的。SqlDataReader有个属性IsDbNull用来识别从数据库返回的记录是不是空。
这里有个使用这种方法的例子。
public class Person
{
private string _fullName;
private int _age;
private bool _hasAge;
public string FullName
{
get{return _fullName;}
set{_fullName = value;}
}
public int Age
{
get
{
if(!_hasAge)
throw new InvalidOperationException("Value not set");
return _age;
}
set
{
_age = value;
_hasAge = true;
}
}
public bool HasAge
{
get{return _hasAge;}
}
public void Load(SqlDataReader dr)
{
if(dr["FullName"].IsDbNull)
_fullName = "";
else
_fullName = dr["FullName"].ToString();
_hasAge = dr["Age"].IsDbNull;
if(!_hasAge)
_age = (int)dr["Age"];
}
}
上面的代码是一个用标记来识别是否是空值的例子。 这里代码使用了HasAge属性来区别Age是否有值。 当对Age赋了值类就将这个标记设置为true,你在使用Age属性的时候总需要验证HasAge的值。
定义一个Nullable类型
一些开发人员更进一步定义了他们自己使用的类型,考虑下面的代码。
public struct NullableInt
{
bool isNull;
public bool IsNull { get { return isNull;}}
bool value;
public int Value {
get { return value;}
set { @value=value;isNull=false;}
}
public NullableInt()
{
isNull = true;
}
// etc.
}
使用这个NullableInt类型代替标准的int类型,我们可以获取对数据更多的控制。上面这种方法的一个优势是你也可以定义在值类型中存在但是在上面代码中没有的DefaultValue字段。如果你要定义这个字段,你需要考虑怎样才是最合理的行为,比如创建一个类的实例,你可能想默认赋一个缺省值,但是如果需要还可以赋空值。这种方法工作的非常好,并且还可以把空值的情况封装起来,客户端代码无需考虑空值的问题。但是当我们两个NullableInt相加的时候将会出现什么情况呢?当然你可以在类的内部添加运算符的重载实现。当将一个double类型变量转化为NullablInt的时候会麻烦一点,你可以在构造函数中使用简单的语法花费很小的代价实现(你也可以使用运算符重载,这里将给你演示使用构造函数。)
public NullableInt(double d)
{
Value = d;
}
客户端代码将会用下面的方式来使用。
NullableInt i;
double d = 2.5;
i = new NullableInt(d);
Console.WriteLine(i.ToString()); //will print 2
在传统模式下你可能会简单写下面类似的代码。
int i;
double d = 2.5;
i = d;
Console.WriteLine(i.ToString()); //will print 2
这种方法功能非常强大,但是在一些情况下也不能胜任,因为不是所有的值都总是允许空值的,而且自定义类型需要开发人员的特殊编码培训。
.NET2.0 Nullable类型简介
既然第三种方法的限制让你恐惧,我告诉你个好消息。C#2.0提供了对基于泛型的并且有简洁语法支持的所有值类型的Nullable版本解决了这个长期以来一直未能解决的问题。
Nullalbe类型的变量可以代表所有的值类型和对应的空值。定义值类型为nullable类型的方法是使用泛型Ssytem.Nullable<T>.因此定义一个nullable整数的语法是:
System.Nullable<int> i;
这种方法看起来有点冗长,因此C#开发组给我们提供了一种空类型的简洁的语法形式 [值类型]?。因此System.Nullable<T>和T?是可以互换的,下面的语句是等效的并且都会声明一个nullable的整型变量。
System.Nullable<int> i;
Or
int? i;
用这种语法你可以声明任何值类型为nullable类型。你将会在下面看到更多的例子。
理解System.Nullable<T>类型
在我深入讲解nullable类型的不同特性和好处之前,理解一下.NET中nullable类型的行为是有好处的。我已经提到这种行为的核心是泛型。如果想了解更多关于泛型的信息你可以看下面一篇很好的文章:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/csharp_generics.asp
ystem.Nullable<T>是一种使nullable类型充满魔力的泛型。你可以把任何类型声明成nullable。Nullble类型例子:
int? i = -1;
bool? flag = null;
char? chr = 'z';
int?[] MyArray = new int?[10];
注意,T在System.Nullable<T>中永远使值类型。因此下面的代码使不正确的,将会出现编译时的警告(你可能希望出现的是错误,但是实际上我发现只是简单给除一些警告)
string? Message = "Hello, World!"; // Compiler warning
SomeClass? someClass { }; // Compiler warning
System.Nullable<T>有两个属性:HasValue(布尔类型)和Value(这个值和nullable类型代表的值类型相同)。HasValue对于非空的实例来说总是true对于空的实例总是false。当HasValue是true,Value属性返回包含的值。当HasValue是false的时候,如果仕途取得Value属性将会导致InavalidOperationExeption异常。这和你所期望的nullable类型的行为是一致的。
使用Nullable类型
下面的代码演示了如何使用一个nullable的整型。
int? i; // declaration
Console.WriteLine(i.HasValue); // will print false
Console.WriteLine(i.Value); // through InvalidOperationException
i = 18; //initialization
Console.WriteLine(i.HasValue); //true
Console.WriteLine(i.Value); //print 18
检测空类型
正如你在上面的代码中看到的那样,当你创建一个nullable类型的新实例的时候,它的HasValue属性返回false,这意味着(前面提到过),如果视图访问value属性将会导致InvalidOperationException异常。那是不是意味着我们总是需要检测HasValue属性来确定nullable类型变量是否是空呢?绝对不需要,在c#中可以用和比较引用类型一样的方法拿nullable对象和null关键字对比。因此下面的代码是合法的:
if (i == null)
Console.WriteLine("i is null");
在客户代码中,你会经常来检测对象是否为空,如果非空就使用其值,否则根据需要做出绝对。例如在下面的代码中,我定义i为nullabl的整型x为普通的整型。接下来,我检测i是否为空,如果为空,我使用i的默认值。这里顺便演示了在c#2中使用新的关键字default。如果非空,把i的值(value)赋给x。
int? i = 18;
int x;
if(i == null)
x = default(int);
else
x = i.Value;
代码很长,不是吗?在c#2中引进了一种新的运算符??来实现这个。看下面的代码:
int? i = 18;
int x;
x = i?? default(int); //getting default value
or
x = i ?? 18; //defining my own value
结论
记住在.NET1.0中初始化和比较enum,struct类型的空值是多么的困难,你需要编写特殊的代码来检验是否合法。现在使用c#2.0你使用功能强大而且优雅的nullable类型以及与此相关的特殊语法来想检验引用类型一样来检验是否为空。空类型给我们提供了一种强大的机制来处理一下特殊的情况包括数据中的空值和其他一些包含可以不赋值的域的特殊类型。既然这种特性是使用泛型实现的,所以不会带来性能上的重大损失。然而,将一个普通的(值)类型转换为nullable类型是会增加一次底层的操作的。因此nullable类型要慎用。当把nullable类型赋给普通类型和互相操作的时候也会需要一些特殊的考虑。