可空类型

  前面的章节介绍了值类型(大多数基本类型,例如,int、double和所有结构)区别于引用类型(string和任意类)的一种方式:值类型必须包含一个值,它们可以声明之后、赋值之前,在未赋值的状态下存在,但不能使用未赋值的变量。而引用类型可以是null

  有时让值类型为空是很有用的(尤其是处理数据库时),泛型使用System.Nullable<T>类型提供了使值类型为空的一种方式。例如:

    System.Nullable<int> nullableInt;

  这行代码声明了一个变量nullableInt,它可以拥有int变量能包含的任意值,还可以拥有值null。所以可以编写如下的代码:

    nullableInt = new System.Nullable<int>();

  与其他任意变量一样,无论是初始化为null(使用上面的语法),还是通过给它赋值来初始化,都不能在初始化之前使用它。

  可以像测试引用类型一样测试可空类型,看看它们是否为null

    if(nullableInt == null)
    {
        ...
    }

  另外,可以使用HasValue属性:

    if(nullableInt.HasValue)
    {
        ...
    }

  这不适用于引用类型,即使引用类型有一个HasValue属性,也不能使用这种方法,因为引用类型的变量值为null,就表示不存在对象,当然就不能通过对象来访问这个属性,否则会抛出一个异常。

  可以来使用Value属性来查看可空类型的值。如果HasValuetrue,就说明Value属性有一个非空值。但如果HasValuefalse,就说明变量被赋予了null,访问Value属性会抛出System.InvalidOperationException类型的异常。


  可空类型非常有用,以至于修改了C#语法。声明可空类型的变量不使用上述语法,而是使用下面的语法:

    int? nullableInt;

  int?System.Nullable<int>的缩写,但更便于读取。在后面的章节中就使用了这个语法。

  1. 运算符和可空类型

  对于简单类型(如int),可以使用+-等运算符来处理值。而对于对应的可空类型,这是没有区别的:包含在可空类型中的值会隐式转换为需要的类型,使用适当的运算符。这也适用于结构和自己提供的运算符。例如:

    int? op1 = 5;
    int? result = op1 * 2;

  注意,其中result变量的类型也是int?。下面的代码不会被编译:

    int? op1 = 5;
    int result = op1 * 2;   ❌

  为了使上面的代码正常工作,必须进行显式转换:

    int? op1 = 5;
    int result = (int)op1 * 2;   ✅

  或通过Value属性访问值:

    int? op1 = 5;
    int result = op1.Value * 2;

  只要op1有一个值,上面的代码就可以正常运行。如果op1null,就会生成System.InvalidOperationException类型的异常。

  这就引出了下一个问题:当运算表达式中的一个或两个值是null时,例如,下面代码中op1,会发生什么情况?

    int? op1 = null;
    int? op2 = 5;
    int? result = op1 * op2;

  答案是:对于除了bool?外的所有简单可空类型,该操作的结果是null,可以把它解释为“不能计算”。对于结构,可以定义自己的运算符来处理这种情况(详见本章后面的内容)。对于bool?,为&|定义的运算符会得到非空返回值,如表12-1所示。

op1 op2 op1 & op2 op1 | op2
true true true true
true false false true
true null null true
false true false true
false false false false
false null false null
null true null true
null false false null
null null null null

  这些运算符的结果十分符合逻辑,如果不需要知道其中一个操作数的值,就可以计算出结果,则该操作数是否为null就不重要。

  2. ??运算符

  为进一步减少处理可空类型所需的代码量,使可空变量的处理变得更简单,可以使用??运算符。这个运算符称为空接合运算符(null coalescing operator),是一个二元运算符,允许给可能等于null的表达式提供另一个值。如果第一个操作数不是null,该运算符就等于第一个操作数,否则,该运算符就等于第二个操作数。下面的两个表达式的作用是相同的:

    op1 ?? op2
    op1 == null ? op2 : op1

  在这两行代码中,op1可以是任意可空表达式,包括引用类型和更重要的可空类型。因此,如果可空类型是null,就可以使用??运算符提供要使用的默认值,如下所示:

    int? op1 = null;
    int result = op1 * 2 ?? 5;

  在这个示例中,op1null,所以 op1 * 2 也是null。但是,??运算符检测到这个情况,并把值5赋予result。这里要特别注意,在结果中放入int类型的变量result不需要显式转换。??运算符会自动处理这个转换。还可以把??表达式的结果放在int?中:

    int? result = op1 * 2 ?? 5;

  在处理可空变量时,??运算符有许多用途,它也是一种提供默认值的便捷方式,不需要使用if结构中的代码块或容易引起混淆的三元运算符。

  在下面的示例中,将介绍可空类型Vector。

    public class Vector
    {
        public double? R = null;
        public double? Theta = null;

        public double? ThetaRadians
        {
            get
            {
                // Convert degrees to radians.
                return (Theta * Math.PI / 180.0);
            }
        }

        public Vector(double? r, double? theta)
        {
            // Normalize.
            if(r < 0)
            {
                r = -r;
                theta += 180;
            }
            theta = theta % 360;

            // Assign fields.
            R = r;
            Theta = theta;
        }

        public static Vector operator + (Vector op1, Vector op2)
        {
            try
            {
                // Get (x, y) coordinates for new vector.
                double newX = op1.R.Value * Math.Sin(op1.ThetaDadians.Value)
                    + op2.R.Value * Math.Sin(op2.ThetaRadians.Value);
                double newY = op1.R.Value * Math.Cos(op1.ThetaRadians.Value)
                    + op2.R.Value * Math.Cos(op2.ThetaRadians.Value);

                // Convert to (r, theta).
                double newR = Math.Sqrt(newX * newX + newY * newY);
                double newTheta = Math.Atan2(newX, newY) * 180.0 / Math.PI;

                // Return result.
                return new Vector(newR, newTheta);
            }
            catch
            {
                // Return "null" vector.
                return new Vector(null, null);
            }
        }

        public static Vector operator - (Vector op1)
        {
            return new Vector(-op1.R, op1.Theta);
        }

        public static Vector operator - (Vector op1, Vector op2)
        {
            return op1 + (-op2);
        }

        public override string ToString()
        {
            // Get string representation of coordinates.
            string rString = R.HasValue ? R.ToString() : "null";
            string thetaString = Theta.HasValue ? Theta.ToString() : "null";

            // Return (r, theta) string.
            return string.Format("({0}, {1})", rString, thetaString);
        }
    }

  修改 Program.cs 中的代码,如下所示:

    class Program
    {
        Vector v1 = GetVector("vector1");
        Vector v2 = GetVector("vector1");
        Console.WriteLine("{0} + {1} = {2}", v1, v2, v1 + v2);
        Console.WriteLine("{0} - {1} = {2}", v1, v2, v1 - v2);
        Console.ReadKey();
    }

    static Vector GetVector(string name)
    {
        Console.WriteLine("Input {0} magnitude:", name);
        double? r = GetNullableDouble();
        Console.WriteLine("Input {0} angle (in degrees):", name);
        douoble? theta = GetNullableDouble();
        return new Vector(r, theta);
    }

    static double? GetNullableDouble()
    {
        double? result;
        string userInput = Console.ReadLine();
        try
        {
            result = double.Parse(userInput);
        }
        catch
        {
            result = null;
        }
        return result;
    }

  示例的说明

  在这个示例中,创建了一个类Vector,它表示带极坐标(有一个幅值和一个角度)的矢量,如图12-3所示。

  坐标r和Ɵ在代码中用公共字段R和Theta表示,其中Theta的单位是度(°)。ThetaRadians用于获取Theta的弧度值,这是必需的,因为Math类在其静态方法中使用弧度。R和Theta的类型都是double?,所以它们可以为空。

    public class Vector
    {
        public double? R = null;
        public double? Theta = null;

        public double? ThetaRadians
        {
            get
            {
                // Convert degrees to radians.
                return (Theta * Math.PI / 180.0);
            }
        }
    }

  Vector的构造函数标准化R和Theta的初始值,然后赋予公共字段。

    public class Vector
    {
        public double? R = null;
        public double? Theta = null;

        public double? ThetaRadians
        {
            get
            {
                // Convert degrees to radians.
                return (Theta * Math.PI / 180.0);
            }
        }

  Vector的构造函数标准化R和Theta的初始值,然后赋予公共字段。

    public Vector(double? r, double? theta)
    {
        // Normalize.
        if(r < 0)
        {
            r = -r;
            theta += 180;
        }
        theta = theta % 360;
        // Assign fields.
        R = r;
        Theta = theta;
    }

  Vector类的主要功能是使用运算符重载对矢量进行相加和相减运算,这需要一些非常基本的三角函数知识,这里不解释它们。在代码中,重要的是,如果在获取R或ThetaRadians的Value属性时抛出了异常,即其中一个是null,就返回“空”矢量。

    public static Vector operator + (Vector op1, Vector op2)
    {
        try
        {
            // Get (x, y) coordinates for new vector.
           ...
        }
        catch
        {
            // Return "null" vector.
            return new Vector(null, null);
        }
    }

  如果组成矢量的一个坐标是null,该矢量就是无效的,这里用R和Theta都可为null的Vector类来表示。Vector类的其他代码重写了其他运算符,以便扩展相加的功能,使其包含相减操作,再重写ToString(),获取Vector对象的字符串表示。

  Program.cs中的代码测试Vector类,让用户初始化两个矢量,再对它们进行相加和相减。如果用户省略了某个值,该值就解释为null,应用前面提及的规则。

🔚