表达意图

已完成

在上一个单元中,你了解了 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 不是 nullFind 可能返回 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()
  • 构造函数接受默认为 nullT 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 性意图。 在下一个单元中,你会将学到的知识应用到现有项目中。

知识检测

1.

string 引用类型的 default 值是什么?

2.

取消引用 null 的预期行为是什么?

3.

执行此 throw null; C# 代码时会发生什么情况?

4.

关于可为空引用类型,哪种说法最准确?