空傳播

?. 運算子和 ?[...] 運算子稱為空條件運算子 。它有時也被其他名稱引用,例如安全導航運算子

這很有用,因為如果將 .(成員訪問器)運算子應用於計算為 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);

這種差異在某種程度上解釋了為什麼表示式樹中尚不支援空傳播運算子。