C#
.NET
MSIL
IL

C#で「person?.Name」と「person == null ? null : person.Name」は等価じゃない。

C#において

person?.Name」と「person == null ? null : person.Name」は等価じゃありません。

違う見方をすると、

null条件演算子 (?.) において、nullとの比較に 「==」オペレーターでの比較は行われません。(型に対して==で独自定義した処理は呼ばれない)

同様に、null合体演算子 (??)でも同じように、nullとの比較に 「==」オペレーターでの比較は行われません。(型に対して==で独自定義した処理は呼ばれない)

再現・確認コード

これを再現・確認できるコードは次の通りです。

// 本当はEqualsとかも実装するべき
class Person {
  public string Name { get; set; }

  public static bool operator ==(Person lhs, Person rhs) {
    Console.WriteLine("called ==");
    return object.ReferenceEquals(lhs, rhs);
  }

  public static bool operator !=(Person lhs, Person rhs) {
    Console.WriteLine("called !=");
    return !object.ReferenceEquals(lhs, rhs);
  }
}

void Main()
{
    Person personNonNull = new Person { Name = "Taro" };
    Console.WriteLine(personNonNull?.Name); 

    Person personNull = null;
    Console.WriteLine(personNull?.Name);
}

このコードを実行すると、「called ==」というログは表示されません。==オペレーターが呼ばれていないということが確認できますね。

ILの確認 ==を使った場合

void Main()
{
    Person person = new Person { Name = "Taro" };
    string name = person == null ? null : person.Name;
    Console.WriteLine();    
}

このコードのILは、こんな感じ。

IL_0000:  nop         
IL_0001:  newobj      UserQuery+Person..ctor
IL_0006:  dup         
IL_0007:  ldstr       "Taro"
IL_000C:  callvirt    UserQuery+Person.set_Name
IL_0011:  nop         
IL_0012:  stloc.0     // person
IL_0013:  ldloc.0     // person
IL_0014:  ldnull      
IL_0015:  call        UserQuery+Person.op_Equality
IL_001A:  brtrue.s    IL_0024
IL_001C:  ldloc.0     // person
IL_001D:  callvirt    UserQuery+Person.get_Name
IL_0022:  br.s        IL_0025
IL_0024:  ldnull      
IL_0025:  stloc.1     // name
IL_0026:  call        System.Console.WriteLine
IL_002B:  nop         
IL_002C:  ret
IL_0015:  call        UserQuery+Person.op_Equality

に注目してください。 ==の実装が呼ばれていますね。


ILの確認 null条件演算子を使った場合

void Main()
{
    Person person = new Person { Name = "Taro" };
    string name = person?.Name;
    Console.WriteLine();    
}

このコードのILは、こんな感じ。

IL_0000:  nop         
IL_0001:  newobj      UserQuery+Person..ctor
IL_0006:  dup         
IL_0007:  ldstr       "Taro"
IL_000C:  callvirt    UserQuery+Person.set_Name
IL_0011:  nop         
IL_0012:  stloc.0     // person
IL_0013:  ldloc.0     // person
IL_0014:  brtrue.s    IL_0019
IL_0016:  ldnull      
IL_0017:  br.s        IL_001F
IL_0019:  ldloc.0     // person
IL_001A:  call        UserQuery+Person.get_Name
IL_001F:  stloc.1     // name
IL_0020:  call        System.Console.WriteLine
IL_0025:  nop         
IL_0026:  ret

UserQuery+Person.op_Equalityが存在しないことに注目してください。==で定義したした処理は呼ばれないことに注目してください。

まとめ

person?.Name」と「person == null ? null : person.Name」は等価じゃありません。

違う見方をすると、

null条件演算子 (?.) において、nullとの比較に 「==」オペレーターでの比較は行われない。(型に対して==で独自定義した処理は呼ばれない)

同様に、null合体演算子 (??)でも同じように、nullとの比較に 「==」オペレーターでの比較は行われない。(型に対して==で独自定義した処理は呼ばれない)

こちらもどうぞ

関連記事 : Reshaper/Riderの「Possible unintended bypass of lifetime check of underlying Unity engine object」って何ぞや?