C#.NET 中的可空类型






4.87/5 (66投票s)
本文解释了 C#.NET 中可空类型的详细信息和用法。
摘要
本文将帮助您理解 C# 中 Nullable
类型的实现。本文还解释了空值合并运算符(Coalescing Operator)以及 CLR 如何为 Nullable
值类型提供特殊支持。
引言
众所周知,值类型变量不能为 null
。这就是它们被称为值类型的原因。值类型有很多优点,但在某些场景下,我们也需要值类型能够持有 null
。
考虑以下场景
场景 1:您正在从数据库表中检索可为空的整数列数据,而数据库中的值为 null
,您无法将此值赋给 C# 的 int
变量。
场景 2:假设您正在从 UI 绑定属性,但相应的 UI 没有数据。(例如,ASP.NET MVC 或 WPF 中的模型绑定)。在模型中为值类型存储默认值并非一个可行的选项。
场景 3:在 Java 中,java.Util.Date
是一个引用类型,因此,该类型的变量可以设置为 null
。然而,在 CLR 中,System.DateTime
是一个值类型,DateTime
变量不能为 null
。如果一个用 Java 编写的应用程序想要向运行在 CLR 上的 Web 服务传递日期/时间,当 Java 应用程序发送 null
时就会出现问题,因为 CLR 无法表示和操作它。
场景 4:当向函数传递值类型参数时,如果参数的值未知且您不想传递它,您会使用默认值。但默认值并不总是一个好的选择,因为默认值也可能是一个有效的传递参数值,因此不应被特殊对待。
场景 5:当从 XML 或 JSON 反序列化数据时,如果值类型属性需要一个值,而该值在源数据中不存在,处理这种情况会变得很困难。
同样,我们在日常生活中会遇到许多类似场景。
为了解决这些情况,微软在 CLR 中加入了 Nullable
类型的概念。要理解这一点,请看 System.Nullable<T>
类型的逻辑定义。
(请注意,以下代码片段仅为说明目的的逻辑定义,摘自 Jeffrey Richter 编写的《CLR Via C#, 3rd edition》)
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Nullable<T> where T : struct
{
// These 2 fields represent the state
private Boolean hasValue = false; // Assume null
internal T value = default(T); // Assume all bits zero
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 Boolean 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();
}
public static implicit operator Nullable<T>(T value)
{
return new Nullable<T>(value);
}
public static explicit operator T(Nullable<T> value)
{
return value.Value;
}
}
从上面的定义中,您可以轻易地看出:
Nullable<T>
类型也是一个值类型。Nullable
类型是一个struct
(结构体)类型,它持有一个值类型(struct
)和一个名为HasValue
的Boolean
标志,用以指示该值是否为null
。- 由于
Nullable<T>
本身是一个值类型,它相当轻量。一个Nullable<T>
类型实例的大小等于其包含的值类型的大小加上一个boolean
的大小。 nullable
类型的参数T
是struct
,也就是说,您只能将nullable
类型用于值类型。这是完全合理的,因为引用类型已经可以为null
。您也可以将Nullable<T>
类型用于您自定义的struct
。Nullable
类型并不是对所有值类型的扩展。它是一个包含泛型值类型和boolean
标志的struct
。
语法和用法
要使用 Nullable
类型,只需声明带有值类型参数 T
的 Nullable
struct
,并像声明其他值类型一样声明它。
例如
Nullable<int> i = 1;
Nullable<int> j = null;
使用 Nullable
类型的 Value
属性来获取它所持有的值。如定义所述,如果值不为 null
,它将返回值,否则将抛出异常。因此,在使用之前,您可能需要检查值是否为 null
。
Console.WriteLine("i: HasValue={0}, Value={1}", i.HasValue, i.Value);
Console.WriteLine("j: HasValue={0}, Value={1}", j.HasValue, j.GetValueOrDefault());
//The above code will give you the following output:
i: HasValue=True, Value=5
j: HasValue=False, Value=0
可空类型的转换和运算符
C# 也支持使用 Nullable
类型的简单语法。它还支持对 Nullable
实例的隐式转换和强制类型转换。以下示例展示了这一点。
// Implicit conversion from System.Int32 to Nullable<Int32>
int? i = 5;
// Implicit conversion from 'null' to Nullable<Int32>
int? j = null;
// Explicit conversion from Nullable<Int32> to non-nullable Int32
int k = (int)i;
// Casting between nullable primitive types
Double? x = 5; // Implicit conversion from int to Double? (x is 5.0 as a double)
Double? y = j; // Implicit conversion from int? to Double? (y is null)
您可以像对包含的类型一样,对 Nullable
类型使用运算符。
- 如果
Nullable
类型的值被设置为null
,一元运算符(++、--、- 等)将返回null
。 - 如果任一操作数为
null
,二元运算符(+、-、*、/、%、^ 等)将返回null
。 - 对于相等运算符,如果两个操作数都为
null
,表达式的计算结果为true
。如果任一操作数为null
,计算结果为false
。如果两者都不为null
,则按常规进行比较。 - 对于关系运算符(>、<、>=、<=),如果任一操作数为
null
,结果为false
;如果两个操作数都不为null
,则比较它们的值。
请看下面的示例
int? i = 5;
int? j = null;
// Unary operators (+ ++ - -- ! ~)
i++; // i = 6
j = -j; // j = null
// Binary operators (+ - * / % & | ^ << >>)
i = i + 3; // i = 9
j = j * 3; // j = null;
// Equality operators (== !=)
if (i == null) { /* no */ } else { /* yes */ }
if (j == null) { /* yes */ } else { /* no */ }
if (i != j) { /* yes */ } else { /* no */ }
// Comparison operators (< > <= >=)
if (i > j) { /* no */ } else { /* yes */ }
空值合并运算符
C# 提供了一种相当简化的语法来检查 null
,并在变量值为 null
的情况下同时赋另一个值。这可以用于可空类型以及引用类型。
例如,下面的代码:
int? i = null;
int j;
if (i.HasValue)
j = i.Value;
else
j = 0;
//The above code can also be written using Coalescing operator:
j = i ?? 0;
//Other Examples:
string pageTitle = suppliedTitle ?? "Default Title";
string fileName = GetFileName() ?? string.Empty;
string connectionString = GetConnectionString() ?? defaultConnectionString;
// If the age of employee is returning null
// (Date of Birth might not have been entered), set the value 0.
int age = employee.Age ?? 0;
//The Coalescing operator is also quite useful in aggregate function
//while using linq. For example,
int?[] numbers = { };
int total = numbers.Sum() ?? 0;
// Many times, it is required to Assign default, if not found in a list.
Customer customer = db.Customers.Find(customerId) ?? new Customer();
//It is also quite useful while accessing objects like QueryString,
//Session, Application variable or Cache.
string username = Session["Username"] ?? string.Empty;
Employee employee = GetFromCache(employeeId) ?? GetFromDatabase(employeeId);
您还可以将其链接起来,这可以为您节省大量编码。请看下面的例子:
// Here is an example where a developer is setting the address of a Customer.
// The business requirement says that:
// (i) Empty address is not allowed to enter
// (Address will be null if not entered). (ii) Order of precedence of
// Address must be Permanent Address which if null, Local Address which if null,
// Office Address.
// The following code does this:
string address = string.Empty;
string permanent = GetPermanentAddress();
if (permanent != null)
address = permanent;
else
{
string local = GetLocalAddress();
if (local != null)
address = local;
else
{
string office = GetOfficeAddress();
if (office != null)
address = office;
}
}
//With Coalescing Operator, the same can be done in a single expression.//
string address = GetPermanentAddress() ?? GetLocalAddress()
?? GetOfficeAddress() ?? string.Empty;
与嵌套的 if else
链相比,使用空值合并运算符的代码要容易阅读和理解得多。
可空类型的装箱和拆箱
由于我之前提到过 Nullable<T>
仍然是一个值类型,您必须了解 Nullable<T>
类型在装箱和拆箱时的性能。
CLR 在装箱和拆箱 Nullable
类型时执行一条特殊规则。当 CLR 装箱一个 Nullable
实例时,它会检查其值是否被赋为 null
。在这种情况下,CLR 不会做任何操作,只是简单地将 null
赋给该 object
。如果实例不为 null
,CLR 会取其值并像普通值类型一样进行装箱。
在拆箱为 Nullable
类型时,CLR 会检查一个 object
的值是否被赋为 null
。如果是,它简单地将 Nullable
类型的值赋为 null
。否则,它会像通常一样进行拆箱。
// Boxing Nullable<T> is null or boxed T
int? n = null;
Object o = n; // o is null
Console.WriteLine("o is null={0}", o == null); // results to "True"
n = 5;
o = n; // o refers to a boxed Int32
Console.WriteLine("o's type={0}", o.GetType()); // results to "System.Int32"
// Create a boxed int
Object o = 5;
// Unbox it into a Nullable<int> and into an int
int? a = (Int32?) o; // a = 5
int b = (Int32) o; // b = 5
// Create a reference initialized to null
o = null;
// "Unbox" it into a Nullable<int> and into an int
a = (int?) o; // a = null
b = (int) o; // NullReferenceException
为可空类型调用 GetType()
当为 Nullable<T>
类型调用 GetType()
时,CLR 实际上会“说谎”,并返回它所持有的 Nullable
类型的底层类型。因此,您可能无法区分一个已装箱的 Nullable<int>
实际上是 int
还是 Nullable<int>
。
请看下面的示例
int? i = 10;
Console.WriteLine(i.GetType()); // Displays "System.Int32" instead of "System.Nullable<Int32>"
关注点
请注意,为了使文章只专注于 Nullable
类型,我没有讨论装箱和拆箱过程中的内存分配和对象创建细节。您可以通过 Google 搜索获取有关装箱和拆箱的详细信息。
结论
由于 Nullable
类型也是一个值类型并且相当轻量,请不要犹豫地使用它。它在您的数据驱动应用程序中非常有用。
参考文献
- 《CLR via C#, 3rd edition》:Jeffrey Richter
- http://msdn.microsoft.com/en-us/library/1t3y8s4s.aspx
- http://msdn.microsoft.com/en-us/library/2cf62fcy%28VS.80%29.aspx
历史
- 2011年11月1日:1.0 版