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
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    谷歌Google Chrome浏览器打开新的标签页设置指定固定网址
    Vue子组件和父组件、子组件调用父组件的方法、父组件调用子组件方法、子组件与父组件间的传值
    查询Linux服务器出口IP、curl命令查询Linux公网出口IP、Windows服务器查询出口IP
    mysql查询是对字段进行补0操作,可用于树状结构整体排序
    mysql批量update更新,mybatis中批量更新操作
    CentOS 6.8下网卡配置、桥接模式和NAT连接模式、VMware虚拟机克隆网卡配置
    杂七杂八
    解决SpringMVC拦截器中Request数据只能读取一次的问题
    Redis安装教程及可视化工具RedisDesktopManager下载安装
    JAVA获取客户端请求的当前网络ip地址(附:Nginx反向代理后获取客户端请求的真实IP)
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13868280.html
Copyright © 2011-2022 走看看