深入理解C#(第3版)-- 【C#2】第4章 可空类型(学习笔记)

4.1  没有值时怎么办

4.1.1  为什么值类型的变量不能是null

对于一个引用类型的变量来说,其值是一个引用。而值类型变量的值是它本身的真实数据。可以认为,一个非空引用值提供了访问一个对象的途径。然而,null相当于一
个特殊的值,它意味着我不引用任何对象。

如果把引用想象为URL,null就大致相当于about:blank 。内存中用全零来表示null(这也解释了为什么所有引用类型的默认值是null——清除一整块内存的开销最低,所以对象选择用这种方式来初始化),但它本质上采用的是和其他引用一样的方式来存储的。

4.1.2 在C#1中表示空值的模式

模式1 :魔值

第一种模式是牺牲一个值来表示空值,主要是作为DateTime 的解决方案,因为很少有人希望自己的数据库中真的包含公元元年中的某个日期。

模式2 :引用类型包装

第二个解决方案可以采取两种形式。较简单的形式是直接用object 作为变量类型,并根据需要进行装箱和拆箱。较复杂(同时更吸引人)的形式是假定值类型A 可空,就为它准备一个引用类型B 。在引用类型B 中,包含值类型A 的一个实例变量。B 中还声明了隐式转换操作符,允许将B 转换成A ,以及将A 转换成B 。

虽然允许直接使用null,但都要求在堆上创建对象。

模式3 :额外的布尔标志

最后一种模式的基本思路是使用一个普通的值类型的值,同时用另一个值(一个布尔标志)来表示值是“真正”存在,还是应该被忽略。同样,有两种方式来实现这个解决方案。要么在代码中维护两个单独的变量,要么将“值和标志”封装到另一个值类型中。

这种方式存在相同的缺点:针对想要处理的每个值类型,都必须创建一个新的类型。最后一种模式(采用封装方式)实际就是C# 2 的可空类型的工作方式。

4.2  System.Nullable<T> 和System.Nullable

可空类型的核心部分是System.Nullable<T>

4.2.1  Nullable<T>简介

Nullable<T> 是一个泛型类型。类型参数T有一个值类型约束,所以不能使用Nullable<Stream> 。

不能使用另一个可空类型作为实参,Nullable<Nullable<int>> 是不允许的。

对于任何具体的可空类型来说,T的类型称为可空类型的基础类型(underlying type)。

记住,Nullable<T>仍然为值类型。

最后,框架提供了两个转换。首先,是T 到Nullable<T> 的隐式转换。转换结果为一个HasValue 属性为true的实例。同样,Nullable<T> 可以显式转换为T ,其作用与Value属性相同,在没有真正的值可供返回时将抛出一个异常。

包装(wrapping)和拆包(unwrapping)

将T 的实例转换成Nullable<T> 的实例的过程在C#语言规范中称为包装,相反的过程则称为拆包。

代码清单4-1  使用Nullable<T> 的各个成员

static void Display(Nullable<int> x)
{
    Console.WriteLine("HasValue: {0}", x.HasValue);

    if (x.HasValue)
    {
        Console.WriteLine("Value: {0}", x.Value);
        Console.WriteLine("Explicit conversion: {0}", (int)x);
    }

    Console.WriteLine("GetValueOrDefault(): {0}",
                      x.GetValueOrDefault());
    Console.WriteLine("GetValueOrDefault(10): {0}",
                      x.GetValueOrDefault(10));
    Console.WriteLine("ToString(): \"{0}\"", x.ToString());
    Console.WriteLine("GetHashCode(): {0}", x.GetHashCode());
    Console.WriteLine();
}

//包装等于5 的值
Nullable<int> x = 5;
x = new Nullable<int>(5);
Console.WriteLine("Instance with value:");
Display(x);

//构造没有值的实例
x = new Nullable<int>();
Console.WriteLine("Instance without value:");
Display(x);

4.2.2  Nullable<T>装箱和拆箱

请牢记,Nullable<T> 是一个结构——一个值类型!

已装箱的值可以拆箱成普通类型,或者拆箱成对应的可空类型。

代码清单4-2  可空类型的装箱和拆箱行为

Nullable<int> nullable = 5;
//装箱成“有值的可空类型的实例” 
object boxed = nullable;
Console.WriteLine(boxed.GetType());

//拆箱成非可空变量
int normal = (int)boxed;
Console.WriteLine(normal);

//拆箱成可空变量
nullable = (Nullable<int>)boxed;
Console.WriteLine(nullable);

//装箱成“没有值的可空类型的实例”
nullable = new Nullable<int>();
boxed = nullable;
Console.WriteLine(boxed == null);

//拆箱成可空变量
nullable = (Nullable<int>)boxed;
Console.WriteLine(nullable.HasValue);

4.2.3  Nullable<T>实例的相等性

Nullable<T> 覆盖了object.Equals(object) 

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

 如果first没有值,second为null,它们就是相等的;
 如果first没有值,second不为null,它们就是不相等的;
 如果first有值,second为null,它们就是不相等的;
 否则,如果first 的值等于second ,它们就是相等的。

注意,我们不必考虑second是另一个Nullable<T>的情况,因为装箱规则杜绝了此类情况的发生。second的类型是object,所以如果要成为Nullable<T>,它就必须进行装箱。而前面提到,对一个可空实例进行装箱,会创建非可空类型的一个“箱子”,或者返回一个空引用。

4.2.4  来自非泛型Nullable 类的支持

Nullable 类提供了3个方法:

public static int Compare<T>(Nullable<T> n1, Nullable<T> n2)
public static bool Equals<T>(Nullable<T> n1, Nullable<T> n2)

上述每个方法返回的值都遵从.NET的约定:空值与空值相等,小于其他所有值。

System.Nullable的第3个方法不是泛型的!其签名如下:

public static Type GetUnderlyingType(Type nullableType)

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

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

4.3.1  ?修饰符

可空类型的?修饰符是指定可空类型的一种快捷方式。所以,现在不需要写Nullable<byte>,写byte? 就可以了。

4.3.2 使用null进行赋值和比较

C#编译器允许使用null在比较和赋值时表示一个可空类型的空值。

代码清单4-4  Person类的部分代码,其中包含了年龄的计算

class Person
{
    DateTime birth;
    DateTime? death;
    string name;

    public TimeSpan Age
    {
        get
        {
            if (death == null)
            {
                return DateTime.Now - birth;
            }
            else
            {
                return death.Value - birth;//拆包以进行计算
            }
        }
    }

    public Person(string name,DateTime birth,DateTime? death)
    {
        this.birth = birth;
        this.death = death;
        this.name = name;
    }
}
...
Person turing = new Person("Alan Turing ",
                           new DateTime(1912, 6, 23),
                           new DateTime(1954, 6, 7));
Person knuth = new Person("Donald Knuth ", new DateTime(1938, 1, 10), null);

为什么一定要对值进行拆包处理呢?为什么不能直接返回death-birth 呢?

4.3.3  可空转换和操作符

假如一个非可空的值类型支持一个操作符或者一种转换,而且那个操作符或者转换只涉及其他非可空的值类型时,那么可空的值类型也支持相同的操作符或转换,并且通常是将非可空的值类型转换成它们的可空等价物。

1. 涉及可空类型的转换

 null到T?的隐式转换;
 T 到T?的隐式转换;
 T?到T的显式转换。

现在来看看类型所支持的预定义转换和用户自定义转换。例如,int 到long存在一个预定义转换。假如允许从非可空值类型(S)转换成另一个非可空值类型(T),那么同时允许进行以下转换:

 S?到T?(可能是显式或隐式的,具体取决于原始转换);
 S 到T?(可能是显式或隐式的,具体取决于原始转换);
 S?到T(总是显式的)。

对于用户自定义的转换,这些涉及可空类型的额外转换称为提升转换(lifted conversion )

2. 涉及可空类型的操作符
C#允许重载以下操作符:
 一元:+ ++ - -- ! ~ truefalse
 二元:+ - * / % & | ^ << >>
 相等:== !=
 关系:< > <= >=

当为非可空的值类型T重载了上述操作符之后,可空类型T?将自动拥有相同的操作符,只是操作数和结果的类型稍有不同。这些操作符称为提升操作符——不管它们是预定义的操作符(比如对数值类型执行加法运算),还是用户自定义的操作符(比如将一个TimeSpan 加到一个DateTime 上)。提升操作符在使用时存在着一些限制:
 true和false 操作符永远不会被提升,这两个操作符本身就十分少用,所以这个限制对我们的影响不大;
 只有操作数是非可空值类型的操作符才会被提升;
 对于一元和二元操作符(相等和关系操作符除外),返回类型必须是一个非可空的值类型;
 对于相等和关系操作符,返回类型则必须是bool;
 应用于bool?的&和|操作符有单独定义的行为

对于所有操作符,操作数的类型都成为它们的可空等价物。对于一元和二元操作符,返回类型也成为可空类型。如果任何一个操作数是空值,就返回一个空值。相等和关系操作符的返回类型为非可空布尔型。进行相等测试时,两个空值被认为相等,空值和任何非空值被认为不等。

对于关系操作符,如果它的任何一个操作数是空值,那么返回的始终是false。如果没有任何操作数是空值,那么自然应该使用非可空类型的操作符。

表  达  式 经提升的操作符 结  果

-nullInt
-five
five + nullInt
five + five
nullInt == nullInt
five == five
five == nullInt
five == four
four < five
nullInt < five
five < nullInt
nullInt < nullInt
nullInt <= nullInt

int? –(int? x)
int? –(int? x)
int? +(int? x, int? y)
int? +(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <=(int? x, int? y)

null
-5
null
10
true
true
false
false
true
false
false
false
false

在这个表格中,恐怕最让人不解的就是最后一行,尽管这两个空值真的相等(如第5行所示),但却不能认为一个空值“小于或等于”另一个空值。是不是很奇怪?但根据我的经验,这个“矛盾”在实际应用中并不会造成任何问题。

int i = 5;
if (i == null)
{
    Console.WriteLine ("Never goingto happen");
}

编译器看到了==左侧的int 表达式,又看到了右侧的null,并知道两者都存在到int?的一个隐式转换。由于对两个int?值进行比较是完全有效的,所以代码不会产生错误——而只是警告。

为什么在代码清单4-4中要使用death.Value- birth,而不是直接使用death-birth。根据前面描述的规则,death-birth这个表达式是可以使用的,但结果就会是一个TimeSpan?,而不是一个TimeSpan 。

4.3.4  可空逻辑

表4-2  逻辑操作符&、|、^和!应用于bool? 类型时的真值表

x y x&y x|y x^y !x

true
true
true
false
false
false
null
null
null

true
false
null
true
false
null
true
false
null

true
false
null
false
false
false
null
false
null

true
true
true
true
false
null
true
null
null

false
true
null
true
false
null
null
null
null

false
false
false
true
true
true
null
null
null

记住:这是C#特有的!

有必要记住,本节讨论的提升操作符和转换,还有bool?逻辑,它们都是由C#编译器提供的,而不是由CLR 或者框架本身提供的。

4.3.5  对可空类型使用as操作符

在C# 2 之前,as操作符只能用于引用类型。而在C# 2 中,它也可以用于可空类型。其结果为可空类型的某个值——空值(如果原始引用为错误类型或空)或有意义的值。

static void PrintValueAsInt32(object o)
{
    int? nullable = o as int?;
    Console.WriteLine(nullable.HasValue ?
                      nullable.Value.ToString() : "null");
}
...
PrintValueAsInt32(5);//生成5 
PrintValueAsInt32("some string");//生成null 

4.3.6  空合并操作符

空合并(null coalescing)操作符,用两个操作数之间的?? 符号来表示。

在对first ?? second求值时,大致会经历以下步骤: 

(1) 对first进行求值;
(2) 如结果非空,则该结果就是整个表达式的结果;
(3) 否则求second的值,其结果作为整个表达式的结果。
之所以说“大致”,是因为在语言规范的完整描述中,有许多情况都涉及first和second 的类型转换问题。

下面的代码是完全合法的:

int? a = 5;
int b = 10;
int c = a ?? b;

??操作符的另外两个特性增强了它的可用性。首先,它并非只能用于可空值类型,还能应用于引用类型。

送货问题时,为了找到联系人,C# 1 的代码可能会这样写:

Address contact = user.ContactAddress;
if (contact == null)
{
    contact = order.ShippingAddress;
    if (contact == null)
    {
        contact = user.BillingAddress;
    }
}

如果使用?? 操作符,代码就会变得相当直观易懂:

Address contact = user.ContactAddress ??
order.ShippingAddress ??
user.BillingAddress;

4.4  可空类型的新奇用法

4.4.1  尝试一个不使用输出参数(out)的操作

代码清单4-5  TryXXX模式的备选实现

static int? TryParse(string text)
{
    int ret;
    if (int.TryParse(text, out ret))//使用输出参数的经典调用
    {
        return ret;
    }
    else
    {
        return null;
    }
}
...
int? parsed = TryParse("Not valid");//可空调用 

if (parsed != null)
{
    Console.WriteLine ("Parsed to {0}", parsed.Value);
}
else
{
    Console.WriteLine ("Couldn't parse");
}

或者使用元组(Tuple)

4.4.2  空合并操作符让比较不再痛苦

假定已经有一个恰当的Product类型,那么在C# 1 中,可以像下面这样写比较代码:

public int Compare(Product first, Product second)
{
    // Reverse comparison of popularity to sort descending
    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);
}
View Code

代码清单4-6  提供“部分比较”的辅助类

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)
    whereT : class
    {
        return first == second ? 0
               : first == null ? -1
               : second == null ? 1
               : new int? ();
    }
}


public int Compare(Product first, Product second)
{
    return PC.ReferenceCompare(first, second) ??
           // Reverse comparison of popularity to sort descending
           PC.Compare(second.Popularity, first.Popularity) ??
           PC.Compare(first.Price, second.Price) ??
           PC.Compare(first.Name, second.Name) ??
           0;
}
View Code

空值和条件操作符

在第二个Compare方法中,我在返回空值时使用了new int?(), 而不是null,你可能会对此感到诧异。但是条件操作符要求它的第二个和第三个操作数
要么为相同的类型,要么存在隐式转换。

posted @ 2019-10-12 11:45  FH1004322  阅读(213)  评论(0)    收藏  举报