首先论述另一个较简单的泛型类型(nullable type):可空类型,解决值类型的一个小问题。
12.2.1 可空类型
在前面的章节中,介绍了值类型(大多数基本类型,例如int、double和所有的结构)区别于引用类型(string和所有的类)的一种方式:值类型必须包含一个值,它们可以在声明之后、赋值之前,在未赋值的状态下存在,但不能以任何方式使用。而引用类型可以是null。
有时让值类型为空是很有用的,泛型使用System.Nullable<T>类型提供了使值类型为空的一种方式。例如:
System.Nullable<int> nullableInt;
这行代码声明了一个变量nullableInt,它可以拥有int变量能包含的任意值,还可以拥有值null。所以可以编写下面的代码:
nullableInt = null;
如果nullableInt是一个int类型的变量,上面的代码是不能编译的。
前面的赋值等价于:
nullableInt = new System.Nullable<int>();
与其他变量一样,无论是初始化为null(使用上面的语法),还是通过给它赋值来初始化,都不能在初始化之前使用它。
可以像测试引用类型一样,测试可空类型,看看它们是否为null:
if (nullableInt == null)
{
...
}
另外,可以使用HasValue属性:
if (nullableInt.HasValue)
{
...
}
这不适用于引用类型,即使引用类型有一个HasValue属性,也不能使用这种方法,因为引用类型的变量值为null,就表示不存在对象,当然就不能通过对象来访问这个属性,此时会抛出一个异常。
使用Value属性可以查看引用类型的值。如果HasValue是true,就说明Value属性有一个非空值。但如果HasValue是false,就说明变量被赋予了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;
只要op1有一个值,上面的代码就可以正常运行,如果op1是null,就会生成System.Invalid OperationException类型的异常。
这就引出了下一个问题:当运算等式中的一个或两个值是null时,例如上面代码中的op1,会发生什么情况?答案是:对于除了bool?之外的所有简单可空类型,该操作的结果是null,可以把它解释为“不能计算”。对于结构,可以定义自己的运算符来处理这种情况(详见本章后面的内容)。对于bool?,为&和 | 定义的运算符会得到非空返回值,如表12-1所示。
表 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和不是null时的默认值,其用法如下:
int? op1 = null;
int result = op1 * 2 ?? 5;
在这个示例中,op1是null,所以op1*2也是null。但是,??运算符检测到这个情况,并把值5赋予result。这里特别要注意,在结果中放入int类型的变量result不需要显式转换。??运算符会自动处理这个转换。可以把??等式的结果放在int?中:
int? result = op1 * 2 ?? 5;
在处理可空变量时,??运算符有许多用途,它也是提供默认值的一种方便方式,不需要使用if结构中的代码块。
在下面的示例中,将介绍可空类型Vector。
试试看:可空类型
(1) 在目录C:/BegVCSharp/Chapter12下创建一个新控制台应用程序项目Ch12Ex01。
(2) 使用VS快捷方式,在文件Vector.cs中添加一个新类Vector。
(3) 修改Vector.cs中的代码,如下所示:
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.ThetaRadians.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);
}
}
(4) 修改Program.cs中的代码,如下所示:
class Program
{
public static void Main(string[] args)
{
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();
}
public static Vector GetVector(string name)
{
Console.WriteLine("Input {0} magnitude:", name);
double? r = GetNullableDouble();
Console.WriteLine("Input {0} angle (in degrees):", name);
double? theta = GetNullableDouble();
return new Vector(r, theta);
}
public static double? GetNullableDouble()
{
double? result;
string userInput = Console.ReadLine();
try
{
result = double.Parse(userInput);
}
catch
{
result = null;
}
return result;
}
}
(5) 执行应用程序,给两个矢量(vector)输入值,结果如图12-1所示。
图 12-1
(6) 再次执行应用程序,这次跳过四个值中的至少一个,结果如图12-2所示。
图 12-2
示例的说明
在这个示例中,创建了一个类Vector,它表示带极坐标(有一个幅值和一个角度)的矢量,如图12-3所示。
图 12-3
坐标r和_在代码中用公共字段R和Theta表示,其中Theta的单位是度(°)。ThetaRad用于获取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 Vector(double? r, double? theta)
{
// Normalize.
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
// Assign fields.
R = r;
Theta = theta;
}
Vector类的主要功能是使用运算符重载对矢量进行相加和相减,这需要一些非常基本的三角函数知识,这里不解释它们。在代码中,重要的是,如果在获取R或ThetaRad的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,应用前面提及的规则。