C# 语言空传播

示例

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

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

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


基本

var teacherName = classroom.GetTeacher().Name;
//如果GetTeacher()返回null,则引发NullReferenceException

如果classroom没有老师,GetTeacher()可以返回null。当它是null并且Name访问该属性时,NullReferenceException将抛出a。

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

var teacherName = classroom.GetTeacher()?.Name;
// 如果GetTeacher()返回null,则teacherName为null

随后,如果classroom也可能是null,我们也可以将该语句写为:

var teacherName = classroom?.GetTeacher()?.Name;
//如果GetTeacher()返回null或教室为null,则teacherName为null

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

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

bool hasCertification = classroom.GetTeacher().HasCertification;
// 编译没有错误,但可能在运行时抛出NullReferenceException

bool hasCertification = classroom?.GetTeacher()?.HasCertification;
//编译时错误:从布尔隐式转换?不允许喝

bool? hasCertification = classroom?.GetTeacher()?.HasCertification;
// 效果很好,如果链的任何部分为null,则hasCertification将为null

bool hasCertification = classroom?.GetTeacher()?.HasCertification.GetValueOrDefault();
// 必须从可为空的值中提取值以分配给值类型变量

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

可以将空条件运算符与空合并运算符(??)组合起来如果表达式解析为null,则返回默认值。使用我们上面的例子:

var teacherName = classroom?.GetTeacher()?.Name ?? "No Name";
//GetTeacher()时teacherName将为“No Name” 
// 返回null或教室为null或名称为null

与索引器一起使用

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

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

在上面的示例中:

  • 首先 ?. 确保classroom不是null。

  • 其次 ? 确保整个Students集合不为null。

  • 最后,?.之后,索引器确保[0]索引器未返回null对象。应该注意的是,这个操作仍然可以抛出IndexOutOfRangeException。


与void函数配合使用

空条件运算符也可以与void函数一起使用。但是,在这种情况下,该语句的计算结果不会为null。它将防止出现NullReferenceException。

List<string> list = null;
list?.Add("hi");          // 不计算为空

与事件调用一起使用

假设以下事件定义:

private event EventArgs OnCompleted;

传统上,在调用事件时,最好的做法是检查事件是否null在没有订阅者出现的情况下进行:

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

由于引入了空条件运算符,因此可以将调用减少为一行:

OnCompleted?.Invoke(EventArgs.Empty);

局限性

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

// 错误:分配的左侧必须是变量,属性或索引器
Process.GetProcessById(1337)?.EnableRaisingEvents = true;
// 错误:该事件只能出现在+ =或-=的左侧
Process.GetProcessById(1337)?.Exited += OnProcessExited;

局限性

注意:

int? nameLength = person?.Name.Length;    // 如果'person'为空则安全

不同于:

int? nameLength = (person?.Name).Length;  // 避免这个

因为前者对应于:

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;
        }
    }
}

输出:

Null propagation
I was read
0
Ternary
I was read
I was read
0

为了避免多次调用,等效方法是:

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

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