可空类型

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

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

  1. System.Nullable<int> nullableInt;

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

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

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

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

  1. if(nullableInt == null)
  2. {
  3. ...
  4. }

  另外,可以使用HasValue属性:

  1. if(nullableInt.HasValue)
  2. {
  3. ...
  4. }

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

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

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

  1. int nullableInt;

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

  1. 运算符和可空类型

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

  1. int op1 = 5;
  2. int result = op1 * 2;

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

  1. int op1 = 5;
  2. int result = op1 * 2;

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

  1. int op1 = 5;
  2. int result = (int)op1 * 2;

  或通过Value属性访问值:

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

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

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

  1. int op1 = null;
  2. int op2 = 5;
  3. int result = op1 * op2;

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

op1op2op1 & op2op1 | op2
truetruetruetrue
truefalsefalsetrue
truenullnulltrue
falsetruefalsetrue
falsefalsefalsefalse
falsenullfalsenull
nulltruenulltrue
nullfalsefalsenull
nullnullnullnull

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

  2. 运算符

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

  1. op1 op2
  2. op1 == null op2 : op1

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

  1. int op1 = null;
  2. int result = op1 * 2 5;

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

  1. int result = op1 * 2 5;

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

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

  1. public class Vector
  2. {
  3. public double R = null;
  4. public double Theta = null;
  5. public double ThetaRadians
  6. {
  7. get
  8. {
  9. // Convert degrees to radians.
  10. return (Theta * Math.PI / 180.0);
  11. }
  12. }
  13. public Vector(double r, double theta)
  14. {
  15. // Normalize.
  16. if(r < 0)
  17. {
  18. r = -r;
  19. theta += 180;
  20. }
  21. theta = theta % 360;
  22. // Assign fields.
  23. R = r;
  24. Theta = theta;
  25. }
  26. public static Vector operator + (Vector op1, Vector op2)
  27. {
  28. try
  29. {
  30. // Get (x, y) coordinates for new vector.
  31. double newX = op1.R.Value * Math.Sin(op1.ThetaDadians.Value)
  32. + op2.R.Value * Math.Sin(op2.ThetaRadians.Value);
  33. double newY = op1.R.Value * Math.Cos(op1.ThetaRadians.Value)
  34. + op2.R.Value * Math.Cos(op2.ThetaRadians.Value);
  35. // Convert to (r, theta).
  36. double newR = Math.Sqrt(newX * newX + newY * newY);
  37. double newTheta = Math.Atan2(newX, newY) * 180.0 / Math.PI;
  38. // Return result.
  39. return new Vector(newR, newTheta);
  40. }
  41. catch
  42. {
  43. // Return "null" vector.
  44. return new Vector(null, null);
  45. }
  46. }
  47. public static Vector operator - (Vector op1)
  48. {
  49. return new Vector(-op1.R, op1.Theta);
  50. }
  51. public static Vector operator - (Vector op1, Vector op2)
  52. {
  53. return op1 + (-op2);
  54. }
  55. public override string ToString()
  56. {
  57. // Get string representation of coordinates.
  58. string rString = R.HasValue R.ToString() : "null";
  59. string thetaString = Theta.HasValue Theta.ToString() : "null";
  60. // Return (r, theta) string.
  61. return string.Format("({0}, {1})", rString, thetaString);
  62. }
  63. }

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

  1. class Program
  2. {
  3. Vector v1 = GetVector("vector1");
  4. Vector v2 = GetVector("vector1");
  5. Console.WriteLine("{0} + {1} = {2}", v1, v2, v1 + v2);
  6. Console.WriteLine("{0} - {1} = {2}", v1, v2, v1 - v2);
  7. Console.ReadKey();
  8. }
  9. static Vector GetVector(string name)
  10. {
  11. Console.WriteLine("Input {0} magnitude:", name);
  12. double r = GetNullableDouble();
  13. Console.WriteLine("Input {0} angle (in degrees):", name);
  14. douoble theta = GetNullableDouble();
  15. return new Vector(r, theta);
  16. }
  17. static double GetNullableDouble()
  18. {
  19. double result;
  20. string userInput = Console.ReadLine();
  21. try
  22. {
  23. result = double.Parse(userInput);
  24. }
  25. catch
  26. {
  27. result = null;
  28. }
  29. return result;
  30. }
  示例的说明  在这个示例中,创建了一个类Vector,它表示带极坐标(有一个幅值和一个角度)的矢量,如图12-3所示。

 12.2.1 可空类型  - 图1

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

  1. public class Vector
  2. {
  3. public double R = null;
  4. public double Theta = null;
  5. public double ThetaRadians
  6. {
  7. get
  8. {
  9. // Convert degrees to radians.
  10. return (Theta * Math.PI / 180.0);
  11. }
  12. }
  13. }

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

  1. public class Vector
  2. {
  3. public double R = null;
  4. public double Theta = null;
  5. public double ThetaRadians
  6. {
  7. get
  8. {
  9. // Convert degrees to radians.
  10. return (Theta * Math.PI / 180.0);
  11. }
  12. }

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

  1. public Vector(double r, double theta)
  2. {
  3. // Normalize.
  4. if(r < 0)
  5. {
  6. r = -r;
  7. theta += 180;
  8. }
  9. theta = theta % 360;
  10. // Assign fields.
  11. R = r;
  12. Theta = theta;
  13. }

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

  1. public static Vector operator + (Vector op1, Vector op2)
  2. {
  3. try
  4. {
  5. // Get (x, y) coordinates for new vector.
  6. ...
  7. }
  8. catch
  9. {
  10. // Return "null" vector.
  11. return new Vector(null, null);
  12. }
  13. }

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

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