聊聊可空类型

一、前言

在C#2.0之前,对于值类型来讲,我们是没法把一个null值赋给一个值变量的,那时,null只专属于引用类型,猜想Null当时的设计就是针对引用类型的,把null赋给引用类型变量时,表明变量不引用任何堆上的对象。即指针为空指针。

由于这种设计,在C#2.0之前,程序员往往面对一个很尴尬的问题,在实际中,我们需要值类型能够为null的情况,最常见的问题体现在数据库设计中,例如一个物料表,我们除了设计一些物料属性之外,可能会设计一些跟踪字段,例如修改人,修改日期(DateTime)

此时,设计人员会将修改人和修改日期设为Nullable,即可空的,这完全是合理的,但当我们查询数据库往映射对象填充DateTime时,尴尬产生了,因为我们很有可能将一个null赋给DateTime类型的变量,而这在当时是不允许的。因此程序员可能要做一些即复杂又影响性能的操作,例如设计一个引用类型对象,装箱折箱,又或者额外设计一个标识字段来做为标志位来判断这个修改日期是否为null。

 由于这些问题的产生,因此CLR在C#2.0中设计了一个全新的泛型类型Nullable<T>来满足这些需求.

二、初窥可空类型

我们先来看一下Nullable<T>的设计:

public struct Nullable<T> where T : struct
{
   …
}

从上面,我们可以得出两个重要的信息:

1、  可空类型也是一个值类型(struct)

2、  泛型参数被约束为值类型(引用类型本身就允许为null)

可空类型没有那么神秘,不要把它当作一个特殊的类型,它就是值类型,与普通的值类型使用方式完全一样。例如,在使用之前,必须初始化变量

可空类型的初始化:

Nullable<Int32> n1;

Nullable<Int32> n2 = null;//n2赋值方式与n3的赋值方式本质一样
Nullable<Int32> n3 = new Nullable<Int32>();

Nullable<Int32> n4 = 1;//n4赋值方式与n5的赋值方式本质一样
Nullable<Int32> n5 = new Nullable<Int32>(1);

可空类型Nullable<T> 与值类型T是完全的不同的一种概念,我们可以理解成Nullable<T>是对T进行了包装,例如Nullable<Int32> n4=1,实际上编译器是调用Nullable的构造器newNullable<Int32>(1),来将Int32包装成Nullable<Int32>

注意:Nullable<Int32> n4=1 这里并不是使用了可空类型的隐式转换(T隐式转换Nullable<T>),我们可以通过IL代码来验证:


Nullable<T>的属性和方法:

//如果存在一个非可空的值,则返回TRUE,否则返回FALSE
public bool HasValue { get; }
//如果存在一个非可空的值,那么Value就表示这个值,如果不存在,则抛出一个InvalidOperationException异常
public T Value { get; } 
//如果存在一个非可空的值,则返回它,如果不存在,则返回一个默认值
public T GetValueOrDefault();
//如果存在一个非可空的值,则返回它,如果不存在,则返回参数defaultValue
public T GetValueOrDefault(T defaultValue);
public static explicit operator T(T? value);//显式转换
public static implicit operator T?(T value);//隐式转换

另外,可空类型还重写了Object的三个虚方法ToString()、GetHashCode、Equals

ToString方法在没有值的情况下,返回空字符串,否则就返回值的ToString结果

GetHashCode方法在没有值的情况下,返回0,否则就返回值的GetHashCode结果

Equals方法,在介绍完后面的装箱拆箱后详细说明

我们看下以下的例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{
    public struct MYStruct
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("不为null的情况");
            Nullable<Int32> n = 1;
            Show(n);
            Console.WriteLine("为null的情况");
            Nullable<Int32> n1 = null;
            Show(n1);
            Console.Read();
        }
        static void Show(Nullable<Int32> n)
        {
            Console.WriteLine("HasValue : {0}", n.HasValue ? "Yes" : "No");
            try
            {
                Console.WriteLine("n.Value :{0}", n.Value);
                Console.WriteLine("explicit conversion :{0}", (Int32)n);
            }
            catch (InvalidOperationException e)
            {
                Console.WriteLine("if n is null,don't use it's value of property");
            }
            Console.WriteLine("n.GetValueOrDefault() :{0}", n.GetValueOrDefault());
            Console.WriteLine("n.GetValueOrDefalut(100) :{0}", n.GetValueOrDefault(100));
            Console.WriteLine("n.ToString() :{0}", n.ToString());
            Console.WriteLine("n.GetHashCode() :{0}", n.GetHashCode());
        }
    }
}

输出结果:

可空类型的语法糖:

Int32 ? i = 3;//等于Nullable<Int32> i=3;

Int32 ?中 ?其实是C#提供的一个语法糖方便开发人员对可空类型的使用,编译器在编译过程中,如果发现T ?这种类型(实际上C#根本没有提供Int32?类型)的,会认为就是Nullable<T>类型

我们看下上面Int32? I=3产生的IL代码:

三、可空类型的装箱与拆箱

可空类型是一个struct,因此在使用的过程中无法避免发生装箱和拆箱操作。

例如,调用GetType方法时,又或者某个期待object类型方法,但实际调用时传递的实参却是可空类型等等情况

装箱:

Nullable<T>在发生装箱时,CLR会检查它是否为null值

1、 可空类型不为null时的装箱:

Nullable<T>发生装箱时,实际上是对T进行装箱,也就是说一个值为3的Nullable<Int32>会装箱成为值为5的一个已装箱的Int32

我们可以通过装箱后值调用GetType查看实际装箱的值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Nullable<Int32> i = 3;
            object boxed = i;
            Console.WriteLine("装箱后的实际类型是:{0}", boxed.GetType());
            Console.Read();
        }
    }
} 

输出结果:

我们可以验证,输出的类型并不是System.Nullable<Int32>,而是System.Int32

2、 可空类型为null时的装箱:

当可空类型为null时,实际上不会进行装箱的动作,但会发生装箱的结果

这句话这样理解,可空类型为null时,装箱过程中不会对null进行在托管堆上的装箱操作

但会返回一个空引用(null)给栈上的引用变量 例如Nullable<Int32> i=null;在对i进行装箱时,不会在堆上产生一个装箱操作(因为i=null,没实际的值可以进行包装),但会返回一个空引用给堆上的object类型引用变量

拆箱:

 

同样,在拆箱的过程,CLR也会检查装箱的值是否为null

1、装箱值不为null

CLR可以将这种情况的装箱值,拆箱成T和Nullable<T>类型

Int32? i = 3;
Object o = i;
Int32 i1 = (Int32)o;//拆箱成Int32
Int32? I2 = (Int32?)o;//拆箱成Int32?

2、装箱值为null

这种情况的装箱值,只能拆箱成Nullable<T>类型

Int32? i = null;
Object o = i;
Int32 i1 = (Int32)o;//错误
Int32? i2 = (Int32?)o;//正确

考虑一下,我们能不能将Nullable<Int32>类型的变量n转型为一个接口类型IComparable<Int32> ?

我们可以查看下Nullable<T>的类型定义,发现,Nullable<T>并没有继承IComparable<T>的接口,理论上,我们是没法将一个Nullable<Int32> 的变量n转型为一个接口类型IComparable<Int32>的,但C#编译器允许这样的代码进行通过编译,例如下面的代码是可以编译通过的:

Int32? n = 3;
Int32? result = ((IComparable<Int32>)n).CompareTo(5);//编译顺利通过
Console.WriteLine(result);//输出-1

四、此null非彼null

我们看一下面一段代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Nullable<Int32> i = null;
            object o = null;
            Console.WriteLine(i.ToString());//输出一个空字符串
            Console.WriteLine(o.ToString());//会抛出一个NullReferenceException异常
            Console.Read();
        }
    }
}

对于o.ToString()会抛出异常,这是很容易理解的,因为o是一个空引用,它不指向任何托管堆上的对象,但i.ToString()为什么可以正常执行呢?它也是一个null,这里我们千万注意,对于可空类型的null与引用类型的null具有完全不一样的意义!此null非彼null,我们可以看下i与o在栈上的内存模型:


从以上的图中,我们可以很清楚的理解,对于可空类型,虽然值为null,但是它在栈上的内存是有实际意义的,可空类型是一个结构体,当把null值赋给可空类型时,结构体中的HasValue会设置为false,但对于引用类型如果设置为null则它在栈上4个字节,全部为0,不指向任何对象。

 

因此我们可以理解为什么对于i.ToString()可以正常输出,而o.ToString()抛出异常

注意:这里用i.ToString()来比较引用类型与可空类型null的区别的前提是因为Nullable<T>重写了object的ToString(),因为值类型如果在没有重写ToString方法情况下,调用时会发生装箱操作,如果Nullable<T>没有重写ToString(),如果可空类型为null,调用ToString()时,同样会抛出NullReferenceException异常,下面会解释为什么

我们再看一段代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Nullable<Int32> i = null;
            object o = null;
            Console.WriteLine(i.GetType());//抛出NullReferenceException异常
            Console.WriteLine(o.GetType());//抛出NullReferenceException异常
            Console.Read();
        }
    }
}

这里i也为null,但为什么也会抛出异常呢?这就得从GetType这个方法说起

GetType方法是Object类型的一个非虚实例方法,值类型在调用时,会发生一个装箱操作,问题就出现在这,装箱,因为可空类型被装箱成一个引用类型,且这个引用变量为null,i.ToString()和o.ToString()一样,会抛出一个NullReferenceException



五、可空类型的相等性

可空类型重写了Equals方法,我们反编译一下,看下它的实现过程

public override bool Equals(object other)
{
    if (!this.HasValue)
    {
        return (other == null);
    }
    if (other == null)
    {
        return false;
    }
    return this.value.Equals(other);
}

从它的实现过程我们总结以下规则

a.Equals(b)

1、a 和 b其中一个为null,返回false

2、a 和 b全部为null,返回true

3、a和 b 都不为null,返回值相等性的结果

 

另外,我们可以像Int、float这些基元类型一样,使用==和!=来进行判断,判断规则和Equals一致

这里大家要注意一下,对于非基元值类型,如果没有重载==和!=操作符,是不能通过==和!=进行相等性的判断的(编译不通过),我们发现可空类型Nullable<T>中,并没有重载==和!=,但是我们发现Int32 a,b中可以通过a==b或者a!=b来判等,这是为何?

原来当非可空值类型T重载了某些操作符后,可空类型T?将自动拥有相同的操作符,只是操作符和结果的类型稍有不同,这些操作符称为“提升操作符”

因此,对于a==b或者a!=b这些操作我们就有了一个合理的解释。首先T这里为Int32,由CLR对于Int32、float、double等这些基元类型支持预定义操作符(它们本身的实现代码中并没有重载!=、==、+、-等这些操作,但是它们是由CLR内部支持的)因此,对于Int32?类型同样拥有!=、==、+、-等这些操作符。

但是如果T本身不支持==和!=或者没有重载这些操作符,那么Nullable<T>也就不能使用==、!=等这些操作符。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{

    /// <summary>
    /// 没有重载==和!=操作符
    /// </summary>
    internal struct MYStruct
    {
        private Int32 i;
        private Int32 j;
        public MYStruct(Int32 i, Int32 j)
        {
            this.i = i;
            this.j = j;
        }
    }
    /// <summary>
    /// 重载了==和!=操作符
    /// </summary>
    internal struct MYNewStruct
    {
        private Int32 i;
        private Int32 j;
        public MYNewStruct(Int32 i, Int32 j)
        {
            this.i = i;
            this.j = j;
        }
        public static bool operator ==(MYNewStruct m1, MYNewStruct m2)
        {
            return m1.Equals(m2);
        }
        public static bool operator !=(MYNewStruct m1, MYNewStruct m2)
        {
            return !m1.Equals(m2);
        }

    }
    class Program
    {
        static void Main(string[] args)
        {
            Int32? a = null;
            Int32? b = null;
            Int32? c = 1;
            Int32? d = 1;

            Console.WriteLine("T为Int32,并且两个可空类型都为null的比较结果");
            Console.WriteLine(a.Equals(b));
            Console.WriteLine(a == b);//可空类型没有重载==和!=,但可使用,这是由CLR内部支持

            Console.WriteLine("T为Int32,并且其中一个可空类型为null的比较结果");
            Console.WriteLine(a.Equals(c));
            Console.WriteLine(a == c);

            Console.WriteLine("T为Int32,并且都不为null的比较结果");
            Console.WriteLine(c.Equals(d));
            Console.WriteLine(c == d);


            MYStruct? m1 = new MYStruct(1, 1);
            MYStruct? m2 = new MYStruct(1, 1);

            Console.WriteLine("T为MYStruct(没有重载==、!=),并且都不为null的比较结果");
            Console.WriteLine(m1.Equals(m2));
            //Console.WriteLine(m1 == m2);//MYStruct没有重载!=和==操作符,因此MYStruct?不能用==和!=比较

            MYNewStruct? mn1 = new MYNewStruct(1, 1);
            MYNewStruct? mn2 = new MYNewStruct(1, 1);

            Console.WriteLine("T为MYNewStruct(重载了==、!=),并且都不为null的比较结果");
            Console.WriteLine(mn1.Equals(mn2));
            Console.WriteLine(mn1 == mn2);//MYNewStruct重载了!=和==操作符,因此MYNewStruct?可以使用==和!=


            Console.Read();
        }
    }
}


输出结果:

注意:以上比较的类型都是可空类型,如果是Nullable<T> a==T b,会先将T b隐式转换成Nullable<T> b 类型后再进行比较

六、??操作符

??是C#中的一种操作符,它是在C#2.0时随着可空类型的引入而引入的

a ?? b 它代表的意义其实等效于 a==null? b:a,即判断左边的值是否为null,如不为null返回左边的值,如为null,则返回右边的值

 

1、??适用于可空类型与可空类型,引用类型与引用类型之间的操作

2、??操作符的返回类型只能是可空类型或者引用类型

3、??操作符不适用于可空类型与引用类型之间的操作,例如a为可空类型,b为引用类型

4、??操作符作用于引用类型之间时,允许a与b不是同一种类型,但要保证a与b之间存在继承关系(继承Object除外),并且返回的结果必须赋值给a与b之间继承链最上的类型

5、??操作符作用于值类型之间时,允许操作可空类型与普通值类型,但要保证??左边的类型为可空类型,并且不为null,普通值类型在右边,当然前提必须是,可空类型的基本类型与普通类型一致

 

下面的例子总结了??的各种用法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Nullable_Demo
{
    public class A
    {
    }
    public class B : A { }
    class Program
    {
        static void Main(string[] args)
        {
            Int32? a = 1;
            Int32 b = 2;
            Int32? c = null;
            object o = null;

            A a1 = new A();
            B b1 = new B();


            Int32? result1 = a ?? c;//返回a
            Int32? result2 = c ?? a;//返回a



            //这里b为Int32,与a的基本变量Int32相符,并且a不为null,且出现在右边,可以编译通过,返回a
            Int32? result3 = a ?? b;

            // Int32? result4 = b ?? a;//无法编译通过

            // Int32? result5 = a ?? o;//无法编译通过,因为a与o的类型不一致

            A result4 = a1 ?? b1;//a1与b1都是引用类型,并且A与B存在继承关系,并且返回值赋给A类型

            // B result6 = b1 ?? a1;//无法编译通过

            //Int32 result5 = a ?? c;//无法编译通过,a??c的结果为Int32?

            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.WriteLine(result3);
            Console.WriteLine(result4);

            Console.Read();
        }
    }
}

输出结果:










  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值