空传播

?. 运算符和 ?[...] 运算符称为空条件运算符 。它有时也被其他名称引用,例如安全导航运算符

这很有用,因为如果将 .(成员访问器)运算符应用于计算为 null 的表达式,程序将抛出 NullReferenceException。如果开发人员使用 ?.(null-conditional)运算符,则表达式将计算为 null 而不是抛出异常。

请注意,如果使用 ?. 运算符且表达式为非 null,则 ?.. 是等效的。

基本

var teacherName = classroom.GetTeacher().Name;
// throws NullReferenceException if GetTeacher() returns null

查看演示

如果 classroom 没有老师,GetTeacher() 可能会返回 null。如果是 null 并且访问了 Name 属性,则会抛出 NullReferenceException

如果我们修改此语句以使用 ?. 语法,则整个表达式的结果将为 null

var teacherName = classroom.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null

查看演示

随后,如果 classroom 也可能是 null,我们也可以将此声明写成:

var teacherName = classroom?.GetTeacher()?.Name;
// teacherName is null if GetTeacher() returns null OR classroom is null

查看演示

这是一个短路示例:当使用空条件运算符的任何条件访问操作求值为 null 时,整个表达式立即求值为 null,而不处理链的其余部分。

当包含空条件运算符的表达式的终端成员是值类型时,表达式求值为该类型的 Nullable<T>,因此不能用作没有 ?. 的表达式的直接替换。

bool hasCertification = classroom.GetTeacher().HasCertification;
// compiles without error but may throw a NullReferenceException at runtime

bool hasCertification = classroom?.GetTeacher()?.HasCertification;
// compile time error: implicit conversion from bool? to bool not allowed

bool? hasCertification = classroom?.GetTeacher()?.HasCertification;
// works just fine, hasCertification will be null if any part of the chain is null

bool hasCertification = classroom?.GetTeacher()?.HasCertification.GetValueOrDefault();
// must extract value from nullable to assign to a value type variable

与 Null-Coalescing 运算符一起使用(??)

如果表达式解析为 null,则可以将空条件运算符与 Null- coilecing Operator??)组合以返回默认值。使用上面的示例:

var teacherName = classroom?.GetTeacher()?.Name ?? "No Name";
// teacherName will be "No Name" when GetTeacher() 
// returns null OR classroom is null OR Name is null

与索引器一起使用

null 条件运算符可以与索引器一起使用 :

var firstStudentName = classroom?.Students?[0]?.Name;

在上面的例子中:

  • 第一个 ?. 确保 classroom 不是 null
  • 第二个 ? 确保整个 Students 系列不是 null
  • 索引器之后的第三个 ?. 确保 [0] 索引器没有返回 null 对象。应该注意的是,这个操作仍然可以投掷一个 IndexOutOfRangeException

与 void 函数一起使用

Null 条件运算符也可以与 void 函数一起使用。但是在这种情况下,该声明不会评估为 null。它只会阻止一个人。

List<string> list = null;
list?.Add("hi");          // Does not evaluate to null

与事件调用一起使用

假设以下事件定义:

private event EventArgs OnCompleted;

在调用事件时,传统上,最好在没有订阅者的情况下检查事件是否为 null

var handler = OnCompleted;
if (handler != null)
{
    handler(EventArgs.Empty);
}

由于引入了空条件运算符,因此可以将调用简化为单行:

OnCompleted?.Invoke(EventArgs.Empty);

限制

Null 条件运算符产生 rvalue,而不是 lvalue,也就是说,它不能用于属性赋值,事件订阅等。例如,以下代码将不起作用:

// Error: The left-hand side of an assignment must be a variable, property or indexer
Process.GetProcessById(1337)?.EnableRaisingEvents = true;
// Error: The event can only appear on the left hand side of += or -=
Process.GetProcessById(1337)?.Exited += OnProcessExited;

陷阱

注意:

int? nameLength = person?.Name.Length;    // safe if 'person' is null

一样的:

int? nameLength = (person?.Name).Length;  // avoid this

因为前者对应于:

int? nameLength = person != null ? (int?)person.Name.Length : null;

而后者对应于:

int? nameLength = (person != null ? person.Name : null).Length;

尽管此处使用三元运算符 ?:来解释两种情况之间的差异,但这些运算符并不等效。通过以下示例可以轻松演示这一点:

void Main()
{
    var foo = new Foo();
    Console.WriteLine("Null propagation");
    Console.WriteLine(foo.Bar?.Length);

    Console.WriteLine("Ternary");
    Console.WriteLine(foo.Bar != null ? foo.Bar.Length : (int?)null);
}

class Foo
{
    public string Bar
    {
        get
        {
            Console.WriteLine("I was read");
            return string.Empty;
        }
    }
}

哪个输出:

空传播
我读了
0
三元
我读了
我读了
0

查看演示

为避免多次调用,等效的是:

var interimResult = foo.Bar;
Console.WriteLine(interimResult != null ? interimResult.Length : (int?)null);

这种差异在某种程度上解释了为什么表达式树中尚不支持空传播运算符。