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)时,编译器会发出警告。它只改变警告,不改变运行时的行为。