表达意图
在上一个单元中,你了解了 C# 编译器如何执行静态分析以帮助防范 NullReferenceException
。 还了解了如何启用可为空上下文。 在此单元中,你将详细了解如何在可为空上下文中明确表达意图。
声明变量
启用可为空上下文后,可以更深入地了解编译器如何看到你的代码。 可以对从已启用可为空上下文生成的警告进行操作,这样就可以明确定义意图。 例如,让我们继续检查 FooBar
代码,并仔细检查声明和赋值:
// Define as nullable
FooBar? fooBar = null;
请注意添加到 FooBar
的 ?
。 这会告诉编译器你明确希望 fooBar
可为空。 如果不打算将 fooBar
设为可为空,但仍想避免该警告,请考虑以下事项:
// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;
此示例将 null 包容 (!
) 运算符添加到 null
,告知编译器你正在将此变量显式初始化为 null。 编译器不会发出有关此引用为 null 的警告。
一种很好的做法是,如有可能,在声明时将不可为 null 的变量指定为非 null
值:
// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");
运算符
如上一个单元所述,C# 定义了多个运算符来表达你围绕可为空引用类型的意图。
null 包容 (!
) 运算符
在上一部分中介绍了 null 包容 (!
) 运算符。 它告知编译器忽略 CS8600 警告。 这是告知编译器你知道自己正在做什么的一种方式,但它附带一个警告,即你应该真正知道自己正在做什么!
在启用可为空上下文的情况下初始化不可为 null 类型时,你可能需要明确请求编译器的包容。 例如,考虑以下代码:
#nullable enable
using System.Collections.Generic;
var fooList = new List<FooBar>
{
new(Id: 1, Name: "Foo"),
new(Id: 2, Name: "Bar")
};
FooBar fooBar = fooList.Find(f => f.Name == "Bar");
// The FooBar type definition for example.
record FooBar(int Id, string Name);
在前面的示例中,FooBar fooBar = fooList.Find(f => f.Name == "Bar");
会生成 CS8600 警告,因为 Find
可能会返回 null
。 这个可能的 null
将赋予 fooBar
,在此上下文中它不可为 null。 但是,在此精心设计的示例中,我们知道 Find
永远不会返回所写的 null
。 可以使用 null 包容运算符向编译器表达此意图:
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;
请注意 fooList.Find(f => f.Name == "Bar")
末尾的 !
。 这会告知编译器你知道 Find
方法返回的对象可能是 null
,这没关系。
可以在方法调用或属性评估之前将 null 包容运算符应用于内联对象。 请考虑另一个精心设计的示例:
List<FooBar>? fooList = FooListFactory.GetFooList();
// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning
static class FooListFactory
{
public static List<FooBar>? GetFooList() =>
new List<FooBar>
{
new(Id: 1, Name: "Foo"),
new(Id: 2, Name: "Bar")
};
}
// The FooBar type definition for example.
record FooBar(int Id, string Name);
在上面的示例中:
GetFooList
是返回可以为 null 的类型List<FooBar>?
的静态方法。fooList
被赋予GetFooList
返回的值。- 编译器在
fooList.Find(f => f.Name == "Bar");
上生成警告,因为赋予fooList
的值可能是null
。 - 假设
fooList
不是null
,Find
可能返回null
,但我们知道它不会返回,因此应用了 null 包容运算符。
可以将 null 包容运算符应用于 fooList
以禁用警告:
FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;
备注
应谨慎使用 null 包容运算符。 仅仅使用它来消除警告意味着你告知编译器不要帮助你发现可能的 null 错误。 请谨慎使用此运算符,并且仅在确定时使用。
有关详细信息,请参阅 !(null 包容)运算符(C# 参考)。
null 合并 (??
) 运算符
使用可为空类型时,可能需要评估它们当前是否为 null
,并采取特定操作。 例如,当可为空类型已被赋予 null
或未初始化时,你可能需要赋予它们非 null 值。 这就是 null 合并运算符 (??
) 有用的地方。
请考虑以下示例:
public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
salesTax ??= DefaultStateSalesTax.Value;
// Safely use salesTax object.
}
在前述 C# 代码中:
salesTax
参数定义为可为空的IStateSalesTax
。- 在方法主体中,
salesTax
使用 null 合并运算符有条件地赋值。- 这可确保如果
salesTax
作为null
传入,它将有一个值。
- 这可确保如果
提示
这在功能上等效于以下 C# 代码:
public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
if (salesTax is null)
{
salesTax = DefaultStateSalesTax.Value;
}
// Safely use salesTax object.
}
以下示例显示了 Null 合并操作符可能很有用的另一个常见的 C# 习惯用法:
public sealed class Wrapper<T> where T : new()
{
private T _source;
// If given a source, wrap it. Otherwise, wrap a new source:
public Wrapper(T source = null) => _source = source ?? new T();
}
上述 C# 代码:
- 定义一个泛型包装类,其中泛型类型参数被限制为
new()
。 - 构造函数接受默认为
null
的T source
参数。 - 包装的
_source
有条件地初始化为new T()
。
有关详细信息,请查看 ?? 和 ??= 运算符(C# 参考)。
null 条件 (?.
) 运算符
使用可以为 null 的类型时,可能需要根据 null
对象的状态有条件地执行操作。 例如:在前面的单元中,FooBar
记录用于通过取消引用 null
来演示 NullReferenceException
。 这是在调用其 ToString
时引起的。 考虑这个相同的示例,但现在应用 null 条件运算符:
using System;
// Declare variable and assign it as null.
FooBar fooBar = null;
// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);
// The FooBar type definition.
record FooBar(int Id, string Name);
上述 C# 代码:
- 有条件地取消引用
fooBar
,将ToString
的结果分配给str
变量。str
变量的类型为string?
(可为空字符串)。
- 它将
str
的值写入什么都没有的标准输出。 - 调用
Console.Write(null)
有效,因此没有警告。 - 如果要调用
Console.Write(str.Length)
,你会收到警告,因为可能会取消引用 null。
提示
这在功能上等效于以下 C# 代码:
using System;
// Declare variable and assign it as null.
FooBar fooBar = null;
// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);
// The FooBar type definition.
record FooBar(int Id, string Name);
可以结合运算符来进一步表达意图。 例如,可以链接 ?.
和 ??
运算符:
FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown
有关详细信息,请参阅 ?. 和 ?[](null 条件)运算符。
总结
在此单元中,你了解了如何在代码中表达为 Null 性意图。 在下一个单元中,你会将学到的知识应用到现有项目中。