65.9K
CodeProject 正在变化。 阅读更多。
Home

C#.NET 中的可空类型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (66投票s)

2011 年 11 月 1 日

CPOL

6分钟阅读

viewsIcon

287499

本文解释了 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)和一个名为 HasValueBoolean 标志,用以指示该值是否为 null
  • 由于 Nullable<T> 本身是一个值类型,它相当轻量。一个 Nullable<T> 类型实例的大小等于其包含的值类型的大小加上一个 boolean 的大小。
  • nullable 类型的参数 Tstruct,也就是说,您只能将 nullable 类型用于值类型。这是完全合理的,因为引用类型已经可以为 null。您也可以将 Nullable<T> 类型用于您自定义的 struct
  • Nullable 类型并不是对所有值类型的扩展。它是一个包含泛型值类型和 boolean 标志的 struct

语法和用法

要使用 Nullable 类型,只需声明带有值类型参数 TNullable struct,并像声明其他值类型一样声明它。NullableTypes/Nullable1.png
例如

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 类型也是一个值类型并且相当轻量,请不要犹豫地使用它。它在您的数据驱动应用程序中非常有用。

参考文献

历史

  • 2011年11月1日:1.0 版
© . All rights reserved.