代码改变世界

【More Effective C#】掩藏在Nullable<T>后的秘密

2010-10-27 08:19  空逸云  阅读(2128)  评论(21编辑  收藏  举报

        对于可空类型,我们并不陌生.例如,当我们为我们的数据库生成一个实体时,例如LINQ2SQL,查看实体类中的字段,经常会发现.我们那些定义为可空的字段的类型后面都会多了个?,如int?string?char?,bool?等等.这就是今天的主角---可空类型.

Nullable<T>

         事实上,所有的可空类型都继承自Nullable<T>类,而编译器为了方便大众,提供了几个常用的可空类型,可空基本类型.可空类型,顾名思义,它是可空的.这意味着,与非可空类型相比.可空类型需要更多的检查.可空类型为非可空类型添加了一种丢失或不可用的状态.日常项目中.可空类型似乎可以为我们带来很多的好处.

public static int? DefaultParse(this string input)
        {
            int answer;
            return int.TryParse(input, out answer) ? answer : default(int?);
        }

  这个扩展方法看起来不会出什么错.实际上.它也能很好的运行.并很大程度上会优化我们的代码.但实际上.它也可能引入一些你可能不能预料到的问题.

Nullable<T>的运算

可空数值类型提供了类似浮点数和NaN之间的语义,所有涉及NaN的比较都将返回false.

double d = 0;
Console.WriteLine(d > double.NaN);              //false
Console.WriteLine(d < double.NaN);              //false
Console.WriteLine(double.NaN > double.NaN);             //false
Console.WriteLine(double.NaN == double.NaN);            //false

与NaN有区别的是.可空类型支持空值之间的等同性比较

int? nullableOne = default(int?);
int? nullableTwo = 0;
int? nullableThree = default(int?);
Console.WriteLine(nullableOne < nullableTwo);    //false
Console.WriteLine(nullableOne > nullableTwo);    //false
Console.WriteLine(nullableOne == nullableThree);    //true

涉及可空数值类型的操作与NaN数值的操作的行为完全相同.

double d = 0;
Console.WriteLine(d + double.NaN);   //NaN
Console.WriteLine(d - double.NaN);   //NaN
Console.WriteLine(d * double.NaN);   //NaN
Console.WriteLine(d / double.NaN);   //NaN


int? nullableOne = default(int?);
int? nullableTwo = default(int);
Console.WriteLine((nullableOne + nullableTwo).HasValue);        //false
//....

Nullable<T>中提供了一个GetValueORDefault实现.可以获取相关可空类型的默认值.这样,我们必须频繁的调用GetValueORDefault,为了减轻我们的工作量与抱怨.减少我们砸键盘的次数.所以,C#中的??出现了.

??操作符

??操作符,也叫空值合并运算符.它是一个二元运算符.左边为可空类型,判读左边的值是否为空,若为空,则返回右边的值,否则返回左边的值.例如

int? result = 123;
return result ?? 321;        //return 123;

int? result = default(int?);
return result ?? 321;       //return 321;

Nullable<T>信息丢失

在很多情况下.我们很Happy的使用着可空类型.但很多时候并难以理清某些特别情况下可空类型返回的结果.

int? f = default(int?);
XmlSerializer x = new XmlSerializer(typeof(int?));
StringWriter t = new StringWriter();
x.Serialize(t, f);

这是一段序列化可空类型int的实现.生成的XML如下.

wps_clip_image-23257

可以看到.类型是int,却没有内容.这样,我们很难看出这段代码其实是由int?类型序列化生成的.信息丢失了.并且在反序列化时,你将十分痛恨自己为什么要使用可空类型.

string storage = t.ToString();
StringReader s = new StringReader(storage);
var f2 = (int)x.Deserialize(s);   //f2不能为空
Console.WriteLine(f2);

除了序列化操作会信息丢失,在类型转换时.也会发生信息的丢失.

int? defaultNullable = default(int?);
string s = defaultNullable.ToString();      //转换成string的空内容""或string.Empty
Console.Write(s);
string s2 = ((object)defaultNullable).ToString();   //装箱后为null,ToString操作不能为空出错
Console.Write(s2);

最小化可空类型的可见范围

经过以上的"血泪史".我们终于可以看清Nullable<T>后面的一些小秘密.可空类型,如一把双刃剑,只有在更好的理解了它的一些特性之后.你才能将其使用得更加的得心应手.发挥其最大的效应.反之,它将给你带来无穷无尽的懊恼很悔恨(不断的咒骂着..该死的可空类型,该死的可空类型.).所以,最小化可空类型的可见范围(避免使用)将让我们的类型更易于使用,且不容易被误用.写出更高质量的代码.