可空值类型

作者:追风剑情 发布于:2021-1-18 10:47 分类:C#

一、可空值类型介绍

我们知道值类型的变量永远不会为 null:它总是包含值类型的值本身。事实上,这正是“值类型”一词的由来。遗憾的是,这在某些情况下会成为问题。例如,设计数据库时,可将一个列的数据类型定义成一个 32 位整数,并映射到 FCL(Framework Class Library)的 Int32数据类型。但是,数据库中的一个列可能允许值为空;也就是说,该列在某一行上允许没有任何值。用 Microsoft.NET Framework 处理数据库数据可能变得很困难,因为在 CLR 中,没有办法将 Int32 值表示成 null。

注意  Microsoft ADO.NET 的表适配器(table adapter)确实支持可空类型。遗憾的是 System.Data.SqlTypes 命名空间中的类型没有用可空类型替换,部分原因是类型之间没有“一对一”的对应关系。例如,SqlDecimal类型最大允许 38 位数,而普通的 Decimal 类型最大只允许 29 位数。此外,SqlString 类型支持它自己的本地化和比较选项,而普通的 String类型并不支持这些。

下面是另一个例子:Java 的 java.util.Date 类是引用类型,所以该类型的变量能设为 null但CLR 的 System.DateTime 是值类型,DateTime 变量永远不能设为 null。如果用 Java写的一个应用程序想和运行 CLR的 Web 服务交流日期/时间,那么一旦 Java 程序发送 null就会出问题,因为 CLR 不知道如何表示 null,也不知道如何操作它。

为了解决这个问题,Microsoft 在 CLR 中引入了可空值类型的概念。为了理解它们是如何工作的,先来看看 FCL 中定义的 System,Nullable<T>结构。以下是 System.Nullablee<T>定义的逻辑表示:

//StructLayout在System.Runtime.InteropServices中
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Nullable<T> where T : struct
{
    //这两个字段表示状态
    private Boolean hasValue = false;//假定null
    internal T value = default(T); //假定所有位都为零

    public Nullable(T value)
    {
        this.value = value;
        this.hasValue = true;
    }

    public Boolean HasValue { get { return hasValue; } }
    public T Value
    {
        get
        {
            if (!hasValue)
            {
                throw new InvalidOperationException(
                    "Nullable object must have a value.");
            }
            return value;
        }
    }

    public T GetValueOrDefault() { return value; }
    
    public T GetValueOrDefault(T defaultValue)
    {
        if (!HasValue) return defaultValue;
        return value;
    }

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

    public override int GetHashCode()
    {
        if (!HasValue) return 0;
        return value.GetHashCode();
    }

    public override string ToString()
    {
        if (!HasValue) return "";
        return value.ToString();
    }

    //隐式转换 T到Nullable<T>
    public static implicit operator Nullable<T>(T value)
    {
        return new Nullable<T>(value);
    }

    //注意:可能引发错误的转换要让程序员显示转换
    //显式转换 Nullable<T>到T
    public static explicit operator T(Nullable<T> value)
    {
        return value.Value;
    }
}

二、C#对可空值类型的支持

C#允许使用相当简单的语法初始化上述两个Nullable<Int32>变量x和y。事实上,C#开发团队的目的是将可空值类型集成到C#语言中,使之成为“一等公民”。为此,C#提供了更清晰的语法来处理可空值类型。C#允许用问号表示法来声明并初始化x和y变量:
Int32? x = 5;
Int32? y = null;
在C#中,Int32? 等价于 Nullable<Int32>。但C#在此基础上更进一步,允许开发人员在可空实例上执行转换和转型。C#还允许向可空实例应用操作符。

译注:作者在这里区分了转换和转型。例如,从Int32的可空版本到非空版本(或相反),称为“转换”。但是,涉及不同基元类型的转换,就称为“转型”或"强制类型转换"。

下面总结了C#如何解析操作符:

●  一元操作符(+, ++, -, --, !, ~)
操作数是null,结果就是null。

●  二元操作符(+,-,*,/,%,&,|,^,<<,>>)
两个操作数任何一个是null,结果就是null。但有一个例外,它发生在将&和|操作符应用于Boolean?操作数的时候。在这种情况下,两个操作符的行为和SQL的三值逻辑一样。对于这两个操作符,如果两个操作数都不是null,那么操作符和平常一样工作。如果两个操作数都是null,结果就是null。特殊行为仅在其中之一为null时发生。下表列出了针对操作数的true,false和null三个值的各种组合,两个操作符的求值情况。

操作数1→
操作数2↓
true false null
true &=true
|=true
&=false
|=true
&=null
|=true
false &=false
|=true
&=false
|=false
&=false
|=null
null &=null
|=true
&=false
|=null
&=null
|=null

●  相等性操作符(==, !=)
两个操作数都是null,两者相等。一个操作数是null,两者不相等。两个操作数都不是null,就比较值来判断是否相等。

●  关系操作符(<,>,<=,>=)
两个操作数任何一个是null,结果就是false。两个操作数都不是null,就比较值。

注意,操作可空实例会生成大量代码。而且操作可空类型的速度慢于非可空类型

可定义自己的值类型来重载各种操作符。使用自己的值类型的可空实例,编译器能正确识别它并调用你重载的操作符(方法)。

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

namespace ConsoleApp19
{
    class Program
    {
        static void Main(string[] args)
        {
            Point? p1 = new Point(1,1);
            Point? p2 = new Point(2,2);
            Console.WriteLine("Are points equal? " + (p1 == p2).ToString());
            Console.WriteLine("Are points not equal? " + (p1 != p2).ToString());
        }

        private static void ConversionsAndCasting()
        {
            //从非可空Int32隐式转换为Nullable<Int32>
            Int32? a = 5;

            //从null隐式转换为Nullable<Int32>
            Int32? b = null;

            //从Nullable<Int32>显式转换为非可空Int32
            Int32 c = (Int32)a;

            //在可空基元类型之间转型
            Double? d = 5; //Int32转型为Double?(d是double值5.0)
            Double? e = b; //Int32?转型为Double?(e是null)
        }

        //C#还允许向可空实例应用操作符
        private static void Operators()
        {
            Int32? a = 5;
            Int32? b = null;

            //一元操作符 (+ ++ - -- ! ~)
            a++; //a=6
            b = -b;//b=null

            //二元操作符 (+ - * / % & | ^ << >>)
            a = a + 3;//a = 9
            b = b * 3;//b = null

            //相等性操作符(== !=)
            if (a == null) { } else { }
            if (b == null) { } else { }
            if (a != b) { } else { }

            //比较操作符(< > <= >=)
            if (a < b) { } else { }
        }

        //注意,操作可空实例会生成大量IL代码,而且操作可空类型的速度慢于非可空类型。
        //例如以下方法
        //private static Int32? NullableCodeSize(Int32? a, Int32? b)
        //{
        //    return (a + b);
        //}

        //编译器生成的IL代码等价于以下C#代码:
        private static Nullable<Int32> NullableCodeSize (
            Nullable<Int32> a, Nullable<Int32> b)
        {
            Nullable<Int32> nullable1 = a;
            Nullable<Int32> nullable2 = b;
            if (!(nullable1.HasValue & nullable2.HasValue))
                return new Nullable<Int32>();
            return new Nullable<Int32>(
                nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());
        }

        internal struct Point
        {
            private Int32 m_x, m_y;
            public Point(Int32 x, Int32 y)
            {
                m_x = x;
                m_y = y;
            }

            public static Boolean operator==(Point p1, Point p2)
            {
                return (p1.m_x == p2.m_x) && (p1.m_y == p2.m_y);
            }

            public static Boolean operator!=(Point p1, Point p2)
            {
                return !(p1 == p2);
            }
        }
    }
}
三、C#的空接合操作符

C#提供了一个“空接合操作符”(null-coalescing operator),即??操作符,它要获取两个操作数。假如左边的操作数不为null,就返回这个操作数的值。如果左边的操作数为null,就返回右边的操作数的值。利用空接合操作符,可以方便地设置变量的默认值。

空接合操作符的一个好处在于,它既能用于引用类型,也能用于可空值类型。以下代码演示了如何使用??操作符:

private static void NullCoalescingOperator() {
	Int32? b = null;
	//下面这行等于:
	//x = (b.HasValue) ? b.Value : 123
	Int32 x = b ?? 123;
	Console.WriteLine(x); // "123"
	
	//下面这行等于:
	//String temp = GetFilename();
	//filename = (temp != null) ? temp : "Untitled";
	String filename = GetFilename() ?? "Untitled";
}

有人争辩说??操作符不过是?:操作符的“语法糖”而已,所以C#编译器团队不应该将这个操作符添加到语言中。实际上,??提供了重大的语法上的改进。 第一个改进是??操作符能更好的支持表达式:
Func<String> f = () => SomeMethod() ?? "Untitled";
相比下一行代码,上术代码更容易阅读和理解。下面这行代码要求进行变量赋值,而且用一个语句还搞不定:
Func<String> f = () => { var temp = SomeMethod();
return temp != null ? temp : "Untitled";};

第二个改进是??在复合情形中更好用。例如,下面这行代码:
String s = SomeMethod1() ?? SomeMethod2() ?? "Untitled";
它比下面这地堆代码更容易阅读和理解:

String s;
var sm1 = SomeMethod1();
if (sm1 != null) s = sm1;
else {
	var sm2 = SomeMethod2();
	if (sm2 != null) s = sm2;
	else s = "Untitled";
}
四、CLR对可空值类型的特殊支持

CLR 内建对可空值类型的支持。这个特殊的支持是针对装箱、拆箱、调用 GetType 和调用 接口方法提供的,它使可空类型能无缝地集成到 CLR 中,而且使它们具有更自然的行为, 更符合大多数开发人员的预期。下面深入研究一下 CLR 对可空类型的特殊支持。

1、可空值类型的装箱

假定有一个逻辑上设为 null 的 Nullable变量。将其传给期待一个 Object 的方法,就必须对其进行装箱,并将对已装箱Nullable的引用传给方法。但对表面上为null的值进行装箱不符合直觉——即使 Nullable变量本身非 null,它只是在逻辑上包含了 null。为了解决这个问题,CLR 会在装箱可空变量时执行一些特殊代码,从表面上维持可空类型的“一等公民”地位。

具体地说,当 CLR 对 Nullable<T>实例进行装箱时,会检查它是否为 null。如果是,CLR不装箱任何东西,直接返回 null。如果可空实例不为 null,CLR 从可空实例中取出值并进行装箱。也就是说,一个值为 5 的 Nullable<Int32>会装箱成值为 5 的已装箱 Int32。以下代码演示了这一行为:

//对Nullable进行装箱,要么返回 null,要么返回一个已装箱的T
Int32? n= null;
Object о= n;//o为nu11
Console.Writeline("o is null={0}", o == null);//"True"
n = 5;
o = n;//o引用一个已装箱的Int32
console.Writeline("o's type={0}", o.GetType());//"System.Int32"

2、可空值类型的拆箱

CLR允许将已装箱的值类型T拆箱为一个T或者Nullable。如果对已装箱值类型的引用是null,而且要把它拆箱为一个Nullable,那么CLR会将Nullable的值设为null。以下代码演示了这个行为:

//创建已装箱的Int32
Object o = 5;

//把它拆箱为一个Nullable和一个Int32
Int32? a = (Int32?)o; //a=5
Int32 b = (Int32)o; //b=5

//创建初始化为null的一个引用
o = null;

//把它“拆箱”为一个Nullable和一个Int32
a = (Int32?)o; //a=null
b = (Int32)o; //NullReferenceException

3、通过可空值类型调用GetType

在Nullable<T>对象上调用GetType,CLR实际会“撒谎”说类型是T,而不是Nullable<T>。以下代码演示了这一行为:
Int32? x = 5;
//下面这行会显示“System.Int32”,而非"System.Nullable<Int32>"
Console.WriteLine(x.GetType());

4、通过可空值类型调用接口方法

以下代码将 Nullable<Int32>类型的变量 n 转型为接口类型 IComparable<Int32>。Nullable<T>不像 Int32 那样实现了 IComparable<Int32>接口,但C#编译器允许这样的代码通过编译,而且 CLR 的校验器也认为这样的代码可验证,从而允许使用更简洁的语法:

Int32? n = 5;
Int32 result =((IComparable) n).CompareTo(5);//能顺利编译和运行
Console.WriteLine(result); // 0

假如 CLR不提供这一特殊支持,要在可空值类型上调用接口方法,就必须写很繁琐的代码。首先要转型为已拆箱的值类型,然后才能转型为接口以发出调用:
Int32 result =((IComparable)(Int32) n).CompareTo(5);// 很繁琐

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号