深入理解C#(十)

*第四章(C# 2:可空类型)

C# 1中表示空值的模式

  1. 魔值:牺牲一个值来表示空值(DBNull.Value表示数据库返回null的情况,这个魔值表示所有类型的空值)
  2. 引用包装类型:直接用object作为变量类型,并根据需要进行装箱和拆箱
  3. 额外的布尔标志:使用一个普通的值类型的值,同时使用另一个布尔标志来表示值是“真正”存在还是被忽略。要么在代码中维护两个单独的变量,要么将值和标志封装到另一个值类型中。

System.Nullable<T>和System.Nullable

Nullable<T>是一个有着值类型约束的泛型类型。T的类型称为可空类型的基础类型(underlying type)。

Nullable<T>有两个重要属性:HasValue和Value。就是采用上面第三种实现模式。

Nullable<T>仍为值类型,值将直接包含一个bool和一个int,而不是其他对象的引用

Nullable<T>引入一个名为GetValueOrDefault的新方法。存在值就返回值否则返回默认值/自己指定的返回值。

Nullable<T>覆盖了其他方法:GetHashCode,ToString和Equals。GetHashCode在没有值时返回0,ToString在没有值时返回空字符串

再次强调Nullable<T>是一个结构(值类型),如果把它转换成引用类型,需要对它装箱

调用first.Equals(second)的具体规则:

  • first没有值,second为null,相等
  • first没有值,second不为null,不相等
  • first有值,second为null,不相等
  • 否则,若first的值等于second,相等

System.Nullable类是一个静态类,只包含静态方法。
提供三个有用方法:

1
2
3
public static int Compare<T>(Nullable<T> n1,Nullable<T> n2)
public static bool Equals<T>(Nullable<T> n1,Nullable<T> n2)
public static Type GetUnderlyingType(Type nullableType)

Compare使用Comparer.Default来比较两个基础值,Equals使用EqualityComparer.Default。空值与空值相等,小于其他所有值。

第三个方法如果参数是一个可空类型,方法就返回它的基础类型;否则返回null。

C# 2为可空类型提供的语法糖

?修饰符

Nullable<byte>byte?可相互转换

使用null进行赋值和比较

可空转换和操作符

如果一个非可空的值类型支持一个操作符或一种转换,且那个操作符或转换只涉及其他非可空的值类型时,那么对应可空的值类型也支持同样的操作符或转换。举例:int=》long,int?=》long?

具体细节待看

可空逻辑

对可空类型使用as操作符

在C# 2之前,as只能用于引用类型。在C# 2中,也可以用于可空类型。

空合并操作符(??)

first??second

  1. 对first进行求值;
  2. 如结果非空,该结果就是表达式的结果
  3. 否则求second的值,其结果为整个表达式的结果
    这个操作符还可用于可空值类型+引用类型

    这个操作符是右结合的

用法案例:

假设有一个在线订购系统,有billing address(账单寄送地址),contact address(联系地址),以及shipping address(送货地址)等,任何用户都必须有一个billing address,但contact address是可选的。对于一个订单来说,送货地址也是可选的,默认为billing address。遇到送货问题,要找联系人时。

C# 1实现:

1
2
3
4
5
6
7
8
9
Address contact=user.ContactAddress;
if(contact==null)
{
contact=order.ShippingAddress;
if(contact==null)
{
contact=user.BillingAddress;
}
}

C# 2改进:

1
2
3
Address contact=user.ContactAddress??
order.ShippingAddress??
user.BilllingAddress;

可空类型的新奇用法

尝试一个不使用输出参数的操作

输出参数的常规用法:用一个返回值来判断一个操作是否成功,并用一个输出参数来返回真正的结果。

返回引用类型的方法经常使用这样一种模式:失败时返回null,成功是返回非空值。但是,假如在方法执行成功的前提下,null也是一个有效的返回值,这样就不行了。

1
2
3
4
5
6
7
ArrayList list=hash[key];
if(list==null)
{
list=new ArrayList();
hash[key]=list;
}
list.Add(newItem);

假设HashTable和Dictionary<Tkey,TValue>可以获取一个委托,每次查找到一个不存在的键时就调用这个委托来添加一个新值。使用这个模式适用这种情况。
使用可空类型,能将这种模式扩展至值类型。

具体实例:

1
public static bool TryParse(string s, out Int32 result);

这个int的方法的传统用法如下:

1
2
3
4
5
6
7
8
9
10
int result; 
if (int.TryParse("Not valid", out result))
{
Console.WriteLine("Parsed to {0}", result);
}
else
{
Console.WriteLine("Couldn't parse");
}
Console.Read();

如果我们用可空类型:

class NullableTryParse

​ {
​ static int? TryParse(string data)
​ {
​ int ret;
​ if (int.TryParse(data, out ret))
​ {
​ return ret;
​ }
​ else
​ {
​ return null;
​ }
​ }

1
2
3
4
5
6
7
8
9
10
11
12
13
    static void Main()
{
int? parsed = TryParse("Not valid");
if (parsed != null)
{
Console.WriteLine("Parsed to {0}", parsed.Value);
}
else
{
Console.WriteLine("Couldn't parse");
}
}
}

这样做的好处是它将返回值与解析是否成功封装在一个变量中,还把做和测试分离了。

还有一个好处是可以和空合并操作符配合使用。

小结一下:

上述模式的要点在于:先定义并获取方法返回值(做),再判断返回值(测试)。

如返回值是引用类型,我们可以通过判断是否为null,很容易实现这个模式,但若是值类型,就不好办了(除非我们定义一个特殊值,但这样会导致语义不明,就像魔数一样),所以就用到了可空类型,这样我们也能像使用引用类型一样,使用值类型。

更好的做法:在.NET 4中包含了Tuple类型,使用元祖可以使返回值含义更明确。

空合并操作符简化比较

问题背景:假设要写一个电子商务网站,且有一个产品列表。希望按流行度,价格,名称依次排序。

C# 1的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int Compare(Product first,Product second)
{
int ret=second.Popularity.CompareTo(first.Popularity);
if( ret !=0)
{
return ret;
}
ret=first.Price.CompareTo(second.Price);
if(ret !=0)
{
return ret;
}
return first.Name.CompareTo(second.Name);
}

我们往往这样写Compare方法。但若是属性返回为null,我们在比较前还要先判断非空,这样代码就太复杂了。我们可以用空合并操作符来简化代码。

C# 2 ??操作符实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static class PartialComparer
{
public static int? Compare<T>(T first, T second)
{
return Compare(Comparer<T>.Default, first, second);
}

public static int? Compare<T>(IComparer<T> comparer,T first,T second)
{
int ret = comparer.Compare(first, second);
return ret == 0 ? new int?() : ret;
}

public static int? ReferenceCompare<T>(T first, T second)
where T : class
{
return first == second ? 0
: first == null ? -1
: second == null ? 1
: new int?();
}

public static bool? ReferenceEquals<T>(T first, T second)
where T : class
{
return first == second ? true
: first == null ? false
: second == null ? false
: new bool?();
}
}

先实现一个用于比较的辅助类。可注意ReferenceCompare方法中的条件操作符用法。

现在可以重写Compare方法:

1
2
3
4
5
6
7
8
9
public int Compare(Product first, Product second)
{
return PartialComparer.ReferenceCompare(first, second) ??
// Reverse comparison of popularity to sort descending
PartialComparer.Compare(second.Popularity, first.Popularity) ??
PartialComparer.Compare(first.Price, second.Price) ??
PartialComparer.Compare(first.Name, second.Name) ??
0;
}

最后的0指明前面所有比较都通过,这两个Product相等。也可用Comparer<string>.Default.Compare(first.Name,second.Name)作比较。