Menu

C# 可空类型:int?、?? 空合并运算符、?. 与空值检查

关于 C# 中 null 的一切:可空值类型(int?、Nullable<T>)及其 HasValue、Value 和 GetValueOrDefault,空合并运算符 ?? 和 ??=,空条件运算符 ?.,空值检查,以及 C# 8 引入的可空引用类型(string?)。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

null 表示“没有值”。引用类型(类、字符串、数组)一直都可以存放它。值类型(int、decimal、bool、DateTime、结构体)则不行,除非你用 ? 把它们变成可空类型。C# 还为处理可能缺失的值提供了一小组运算符:??、??= 和 ?.。

可空值类型:int?

在值类型后面加上 ? 就能允许 null。int? 是结构体 Nullable<int> 的简写,它存放一个值,外加一个表示值是否存在的标志:

输出:

False
True
True
5.50
0
3
Rated 4 stars
4

可空值类型的典型来源是表单中的可选字段、查询参数以及数据库中为 NULL 的列:一个带有 DateTime? ShippedAt 的 Order,在这个值为 null 时就表示还没发货。另一种做法是用 -1 或 DateTime.MinValue 这样的魔法值,但它们看起来就像真实数据,迟早会被当成真实数据来用。

int? 只能显式转换为 int,因为这种转换可能会失败。这就引出了可空值类型唯一会抛出的那个异常。

“Nullable object must have a value”

在没有值时读取 .Value,或者把一个为 null 的 int? 强制转换为 int,都会抛出 InvalidOperationException:

输出:

InvalidOperationException: Nullable object must have a value.
0
0
not stocked

stock is int count 是一个模式:它在一步之内检查是否有值,并把值取出放进 count,读起来比 HasValue 加 Value 更顺。模式匹配页面介绍了更多形式。

与 null 进行算术运算和比较

可空值类型上的运算符是提升的:只要有一个操作数为 null,结果就是 null。排序比较(<、>、<=、>=)是例外:它们返回 false,而不是 null。(另一个例外是 bool? 配合 & 和 |,此时 null & false 的结果是 false。)例如:

输出:

True
[]
False
False
True
True

a > 5 和 a <= 5 都是 false,所以当 a 为 null 时,!(a > 5) 和 a <= 5 并不相同。像 if (!(age >= 18)) Deny(); 这样的代码会把缺失的年龄当成未成年,这可能是你想要的,也可能不是。先决定 null 应该代表什么(通常用 ??),再做比较。

空合并运算符 ??

当 a 不为 null 时,a ?? b 的结果是 a,否则是 b。它适用于可空值类型和引用类型,并且从左到右链式求值:

输出:

dark
16px
20
ANONYMOUS

右侧只在需要时才会求值,所以当缓存有值时,cache ?? LoadFromDatabase() 不会去访问数据库。?? 还接受 throw 表达式,可以写出很紧凑的参数检查(参见 throw):

this.repository = repository ?? throw new ArgumentNullException(nameof(repository));

??=(C# 8)

x ??= y 只在 x 为 null 时才把 y 赋给 x。这是延迟初始化的惯用写法:

private List<string> tags;

public void AddTag(string tag)
{
    tags ??= new List<string>();   // C# 8 and later
    tags.Add(tag);
}

在 C# 8 之前,写成 tags = tags ?? new List<string>(); 或 if (tags == null) tags = new List<string>();。

空条件运算符 ?.

a?.Member 只在 a 不为 null 时才对 Member 求值;否则整个表达式就是 null。它把一连串的空值检查变成一个表达式:

输出:

Braga
(no city)
(no customer)
0
True
handler: saved

值得了解的细节:

  • 这个运算符会让链条的剩余部分短路:在 nobody?.Address?.City 中,一旦 nobody 为 null,右边的部分就都不会执行。
  • 当成员返回值类型时,结果会变成可空类型:Orders?.Count 是 int?,所以它需要 ?? 0 才能重新变成 int。
  • 在 C# 14 之前,?. 不能作为赋值的目标:customer?.Name = "x" 在更早的版本中无法通过编译。
  • handler?.Invoke(...) 是触发事件或调用可选回调的标准写法。

在 null 是合理状态的地方使用 ?.。为了避免异常而到处撒 ?.,会把产生意外 null 的那个 bug 藏起来,程序带着缺失的数据继续运行,而不是在出问题的地方失败。

检查 null

if (customer == null) { ... }             // classic
if (customer is null) { ... }             // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... }         // C# 9
if (customer is { } c) { ... }            // C# 8 property pattern: not null, and named

is null 总是执行真正的空值检查,而 == 会调用该类型的运算符重载(如果有的话),所以一个写得不好的重载会让 x == null 给出错误的结果。对于不能为 null 的参数,要尽早失败:

public void Ship(Order order)
{
    ArgumentNullException.ThrowIfNull(order);   // .NET 6+
    // ...
}

可空引用类型(C# 8)

在值类型后面加 ? 会改变类型本身。从 C# 8 起,在引用类型后面加 ? 只改变编译器检查的内容。开启这项功能后(在项目文件中设置 <Nullable>enable</Nullable>,从 .NET 6 起新项目默认开启),string 表示“不应该为 null”,string? 表示“可能为 null”,当代码与之不符时编译器会发出警告:

#nullable enable

public class User
{
    public string Email { get; set; }      // warning CS8618: Non-nullable property 'Email'
                                           // must contain a non-null value when exiting constructor.
    public string? Nickname { get; set; }  // allowed to be null

    public int NicknameLength() => Nickname.Length;
    // warning CS8602: Dereference of a possibly null reference.

    public int SafeLength() => Nickname?.Length ?? 0;   // no warning
}

string? input = Console.ReadLine();
string name = input;                       // warning CS8600: Converting null literal or possible
                                           // null value to non-nullable type.
string checkedName = input ?? "guest";     // no warning
int len = input!.Length;                   // ! tells the compiler "trust me": no warning

编译器会在方法中跟踪你的检查:在 if (input != null) 之后,input 就被视为非 null。当你比分析结果更清楚时,!(null 包容)运算符可以消除警告;每一个 ! 都是编译器无法验证的断言,所以要尽量少用。

有两点能消除大部分困惑。第一,这只是警告:程序的运行结果不变,如果一个 string 来自没有注解的代码,它在运行时仍然可能是 null。第二,string? 不是 Nullable<string>;它没有 HasValue。

常见错误

  • 不检查就用 .Value。 值为 null 时会抛出 InvalidOperationException。使用 ??、GetValueOrDefault() 或 is int n。
  • 以为 !(x > 5) 就是 x <= 5。 当 x 为 null 时并非如此:与 null 进行的每个排序比较都是 false。
  • 用 ?. 掩盖每一个 null。 它只是让症状消失,并把失败的位置推到离原因更远的地方。
  • 把可空引用警告当成噪音。 每条警告都标出了一行可能抛出 NullReferenceException 的代码。修正代码流程,或者用 ? 说明你的意图。
  • 用魔法值代替 null。 用 -1、0 和 DateTime.MinValue 表示“未知”,看起来就像真实数据。改用 int? 和 DateTime?。

常见问题

C# 中 int? 是什么意思?

int? 是 Nullable<int> 的简写:一个也可以是 null 的 int。int、decimal、bool 和 DateTime 这样的值类型通常总是有值;加上 ? 后,它们多了一种“没有值”的状态,表单中未填写的字段或数据库中为 NULL 的列正需要这种状态。用 HasValue 或 != null 检查它,用 Value 或 GetValueOrDefault() 读取它。

C# 中 ?? 的作用是什么?

a ?? b 是空合并运算符:如果 a 不为 null,结果就是 a,否则就是 b。string name = input ?? "guest"; 在一个表达式里就提供了备用值。它可以链式使用(a ?? b ?? c 取第一个非 null 的值),并且 b 只在需要时才会求值。??=(C# 8)只在变量为 null 时才把右侧的值赋给它。

C# 中 ?. 的作用是什么?

a?.B 是空条件运算符:如果 a 为 null,整个表达式就是 null,B 根本不会被求值,所以不会出现 NullReferenceException。它可以链式使用(order?.Customer?.Address?.City),也适用于方法调用(logger?.Log(...))、事件(Changed?.Invoke(...))和索引器(items?[0])。和 ?? 搭配可以提供默认值:order?.Customer?.Name ?? "unknown"。

“Nullable object must have a value”是什么原因造成的?

读取一个为 null 的可空值类型的 .Value,或者把它强制转换为不可空类型((int)maybe),都会抛出带有这条消息的 InvalidOperationException。先检查 HasValue,或者使用 GetValueOrDefault()、?? fallback 或模式:if (maybe is int n) { ... }。

C# 中如何判断 null?

x == null 和 x is null 都可以;is null(C# 7)不会受到重载了 == 的类型影响,所以很多代码库更喜欢用它。否定形式是 x != null,或者 C# 9 中的 x is not null。要拒绝 null 参数,可以用 ArgumentNullException.ThrowIfNull(x)(.NET 6),在较老的代码中则用 _ = x ?? throw new ArgumentNullException(nameof(x));。

C# 中的可空引用类型是什么?

这是 C# 8 的一项编译器功能,用 <Nullable>enable</Nullable> 开启(新建的 .NET 6+ 项目默认开启)。开启后,string 表示“永远不为 null”,string? 表示“可能为 null”,当你可能解引用 null(CS8602)或把 null 赋给不可空变量(CS8600)时,编译器会发出警告。它只改变警告,不改变运行时的行为。

Coddy 编程语言插图

和 Coddy 一起学编程

开始