zoukankan      html  css  js  c++  java
  • 《深入理解C#》整理2-可空类型

    一、没有值怎么办

    以DateTime为例,购物系统中存在发货日期,但在下单未发货的情况下,发货日期应当可为空,但编译器是不允许DateTime变量设置为空的。在C#2之后我们可以使用可空类型,但在C#1中又是如何处理的?

    1、为什么值类型不能为空

    • 对于引用变量来说,其值是一个引用;对于值类型来说,值是它本身真实的数据。非空引用值提供了访问一个对象的途径,然而null意味着它不引用任何对象。
    • 内存中会全用零来表示null,其本质上采用的是和其他引用一样的方式来存储的,引用类型的变量没有在任何地方隐藏额外的bit,这意味着不能将全零值用于一个真正的引用。另外在那么多的活动对象之前,内存早就用光了,这也是为什么null不是有效的值类型的原因

    举例来说:byte变量的值用单独一个字节来存储,可以将值0~255存储到变量中,如果试图将超出这个范围的值存储到其中,那么读取到的就是“垃圾”数据。256个“普通”值加1个null值,总共要处理257个值,没有办法用一个字节存储那么多的值。如果为每个值类型都设置一个额外的标志位判断一个值是null还是一个“真正”的值,此外每次想要使用值时都得对这个标志位进行检查,内存的消耗将急剧增加。

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

    1、魔值

    • 第一种模式是牺牲一个值来表示空值,主要是作为DateTime的解决方案。它有悖于我在前面给出的理由,即假设每个值都能用于一般用途。所以,我们会牺牲一个值(通常是DateTime.MinValue)来表示空值,这个值称为“魔值”。使用魔值的一个好处在于,它不会浪费任何内存,也不需要添加任何新的类型。然而,它要求你谨慎选择一个合适值。一经选定,这个值将永远不能用来表示真正的数据。另外,这个设计是不“优雅”的。

    2、引用类型包装

    • 第二个解决方案可以采取两种形式。较简单的形式是直接用object作为变量类型,并根据需要进行装箱和拆箱。较复杂的形式是假定值类型A可空,就为它准备一个引用类型B。在引用类型B中,包含值类型A的一个实例变量。B中还声明了隐式转换操作符,允许将B转换成A,以及将A转换成B。然它们允许直接使用null,但都要求在堆上创建对象。所以,如果非常频繁地使用这种方式,会造成难以进行垃圾回收。

    3、额外的布尔标识

    • 最后一种模式的基本思路是使用一个普通的值类型的值,同时用另一个值(一个布尔标志)来表示值是“真正”存在,还是应该被忽略。同样,有两种方式来实现这个解决方案。要么在代码中维护两个单独的变量,要么将“值和标志”封装到另一个值类型中。这种方式存在相同的缺点:针对想要处理的每个值类型,都必须创建一个新的类型。另外,如果值因某种原因要进行装箱,那么不管它是否被认为是空值,都要像平时那样进行装箱。采用封装方式也是C#2的可空类型的工作方式。

    二、System.Nullable和System.Nullable

    可空类型的核心部分是System.Nullable,静态类System.Nullable则提供了一些工具方法,可以简化可空类型的使用。

    1、Nullable简介

    • 类型参数T有一个值类型约束,还意味着不能使用另一个可空类型作为实参。对于任何具体的可空类型来说,T的类型称为可空类型的基础类型,如Nullable的基础类型就是int。Nullable最重要的部分就是它的属性,即HasValue和Value。如果存在一个非可空的值,那么Value表示的就是这个值。如果不存在真正的值,就会抛出一个InvalidOperationException。而HasValue是一个简单的Boolean属性,它指出是存在一个真正的值,还是应该将实例视为null。
    • Nullable有两个构造函数。其中,默认构造函数创建“一个没有值的实例”。另一个构造函数则接受T的一个实例作为值。实例一经创建,就是“不易变”的(假如一个类型的实例在创建之后便不能更改,就说这种类型是不易变的)。
    • Nullable引入了一个名为GetValueOrDefault的新方法,它有两个重载方法,如果实例存在值,就返回该值,否则返回一个默认值。其中一个重载方法没有任何参数(在这种情况下会使用基础类型的泛型默认值),另一个重载方法则允许你指定要返回的默认值。
    • Nullable实现的其他方法全都覆盖了现有的方法:GetHashCode、ToString和Equals。GetHashCode会在实例没有值的时候返回0;如果有值,就返回那个值的GetHashCode。ToString在没有值的时候返回空字符串,否则返回那个值的ToString。Equals稍复杂,后面会有说明
    • 最后,框架提供了两个转换。首先,是T到Nullable的隐式转换。转换结果为一个HasValue属性为true的实例。同样,Nullable可以显式转换为T,其作用与Value属性相同,在没有真正的值可供返回时将抛出一个异常

    2、Nullable装箱和拆箱

    只有在涉及装箱和拆箱时,CLR才会让可空类型有一些特殊的行为。其他时候,可空类型使用的是“标准”的泛型、转换、方法调用。Nullable的实例要么装箱成空引用(如果没有值),要么装箱成T的一个已装箱的值(如果有值)。已装箱的值可以拆箱成普通类型,或者拆箱成对应的可空类型Nullable。拆箱一个空引用时,如果拆箱成普通类型,会抛出一个NullReferenceException;但如果拆箱成恰当的可空类型,就会拆箱成没有值的一个实例。

    3、Nullable实例的相等性

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

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

    这些规则与.NET其他地方的相等性规则是一致的。所以,可空实例可以作为字典的键来使用。

    4、来自非泛型Nullable类的支持

    由于历史原因遗留下来的Nullable类提供了3个方法,前两个方法是比较方法:Compare和Equals;Compare使用Comparer.Default来比较两个基础值,Equals使用EqualityComparer.Default。对于没有值的实例,上述每个方法返回的值都遵从.NET的约定:空值与空值相等,小于其他所有值。第三个方法为GetUnderlyingType,如果参数是一个可空类型,方法就返回它的基础类型;否则就返回null。

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

    在C#语言规范中,可空类型是指可以包含空值的类型——如引用类型和Nullable。可空值类型的空值(null value)是指在“HasValue返回false”时的值。或者是“实例没有值”时的值,它是C#特有的,CLI规范和Nullable本身的文档都没有提到它。

    1、?修饰符

    它是指定可空类型的一种快捷方式。Nullable写成byte?就可以了,两者可以互换使用,最终会编译成完全相同的IL。

    2、使用null进行赋值和比较

    建立一个Person类,属性有姓名、出生日期和死亡日期,如果人仍然健在,在这种情况下,死亡日期就要用null来表示。将死亡日期变量同null进行比较时,是在问它的值是否为空值。同样,将null作为DateTime?实例来使用时,实际是通过调用类型的默认构造函数为这个类型创建空值。

    3、可空转换和操作符

    1、涉及可空类型的转换

    假如允许从非可空值类型(S)转换成另一个非可空值类型(T),那么同时允许进行以下转换:

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

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

    2、涉及可空类型的操作符

    C#允许重载以下操作符:

    • 一元:+ ++ - -- ! ~ truefalse
    • 二元:+ - * / % & | ^ << >>
    • 相等:== !=
    • 关系:< > <= >=

    当为非可空的值类型T重载了上述操作符之后,可空类型T?将自动拥有相同的操作符,只是操作数和结果的类型稍有不同。这些操作符称为提升操作符,不管它们是预定义的操作符,还是用户自定义的操作符。提升操作符在使用时存在着一些限制:

    • true和false操作符永远不会被提升,这两个操作符本身就十分少用,所以这个限制对我们的影响不大;
    • 只有操作数是非可空值类型的操作符才会被提升;
    • 对于一元和二元操作符(相等和关系操作符除外),返回类型必须是一个非可空的值类型;
    • 对于相等和关系操作符,返回类型则必须是bool;
    • 应用于bool?的&和|操作符有单独定义的行为,后面会介绍

    image-20201022202505528

    4、可空逻辑

    bool?它的值可能为true、false或null,那意味着使用二元操作符,总共会有9种不同的组合。

    image-20201022203430160

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

    5、对可空类型使用as操作符

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

    6、空合并操作符??

    这个二元操作符在对first ?? second求值时,大致会经历以下步骤:(1) 对first进行求值;(2) 如结果非空,则该结果就是整个表达式的结果;(3) 否则求second的值,其结果作为整个表达式的结果

    它并非只能用于可空值类型,还能应用于引用类型。另外,它的结合性是右结合,这意味着表达式first ?? second ?? third实际相当于first ?? (second ?? third)。如果还有更多的操作数,可以此类推。可以使用任意数量的表达式,并依次对它们求值,遇到第一个非空的结果就停止。

    作者:Jscroop
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    VIJOS-P1340 拯救ice-cream(广搜+优先级队列)
    uva 11754 Code Feat
    uva11426 GCD Extreme(II)
    uvalive 4119 Always an Interger
    POJ 1442 Black Box 优先队列
    2014上海网络赛 HDU 5053 the Sum of Cube
    uvalive 4795 Paperweight
    uvalive 4589 Asteroids
    uvalive 4973 Ardenia
    DP——数字游戏
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13868280.html
Copyright © 2011-2022 走看看