0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

等値演算子のオーバーロードで null にハマった

C# で等値演算子をオーバーロードしたら null が引数に渡されたときハマってしまったので、その流れや解決策をメモ。

確認

まず、 C# では以下のようにして等値演算子(==)をオーバーロードすることができる。

public static bool operator ==(Person a, Person b) {
  return a.Equals(b);
}

しかしこのサンプルコードでは、引数 a が null のとき NullReferenceException が発生する。

ここで正常な挙動を確認すると、以下のようになっている。

// Random クラスを用いているのに特に意味はない.
Random r1 = null;
Random r2 = null;
Console.WriteLine(r1 == r2); // True
Console.WriteLine(r1.Equals(r2)); // NullReferenceException

このことから、サンプルコードのように愚直に Equals メソッドを呼び出すわけにはいかない。

試行

では以下のようにしてみるとどうだろう。

public static bool operator ==(Person a, Person b) {
  if (a == null) {
    return b == null;
  } else {
    return a.Equals(b);
  }
}

このコードは、実行してみれば分かるが、引数 a、b の内容によらず StackOverflowException が発生する。
それは、a や b が null かどうかを判定する条件式で等値演算子を使うことによって、無限ループ(無限再帰)を作り出しているためである。

解決策

再帰呼び出しを回避しつつ、null 判定をすることができるのが、 ReferenceEquals メソッドである。
ReferenceEquals メソッドは 参照の等価性を評価するため、null の変数は参照も null であるから、正しく判定をすることができる。

Random r1 = null;
Random r2 = null;
Console.WriteLine(ReferenceEquals(r1, null)); // True
Console.WriteLine(ReferenceEquals(r2, null)); // True
Console.WriteLine(ReferenceEquals(r1, r2)); // True

以上のことから、等値演算子をオーバーロードする際には、以下のようにすると良い。

public static bool operator ==(Person a, Person b) {
  if (ReferenceEquals(a, null)) {
    return ReferenceEquals(b, null);
  } else {
    return a.Equals(b);
  }
}

非等値演算子

サンプルコードでは省いていたが、等値演算子をオーバーロードするときには、非等値演算子も同時にオーバーロードする必要がある。
ここでは再帰にならないため、以下のように簡潔に実装することができる。

public static bool operator !=(Person a, Person b) {
  return !(a == b);
}

おまけ(Equals)

Equals メソッドをオーバーライドするときにも、同じ方法を使ったほうが良い。
等値演算子をオーバーロードしている状態で以下のように実装すると、引数 obj が null でないとき、何度か再帰呼び出しをすることになる。

public override bool Equals(object obj) {
  Person person = obj as Person ;
  if (person == null) {
    return false;
  } else {
    return person.Id == this.Id;
  }
}

無限ループに陥ることはないので問題はないが、以下のようにして回避することができる。

public override bool Equals(object obj) {
  Person person = obj as Person ;
  if (ReferenceEquals(person, null)) {
    return false;
  } else {
    return person.Id == this.Id;
  }
}

または、そもそも as 演算子ではなく is 演算子を使うことでも回避できる。
(記事を書いていて気づいた)

public override bool Equals(object obj) {
  if (obj is Person person) {
    return person.Id == this.Id;
  } else {
    return false;
  }
}

まとめ

hoge == null ではなく ReferenceEquals(hoge, null) を使おう。

参考

https://docs.microsoft.com/dotnet/api/system.object.referenceequals
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/equality-operators
https://docs.microsoft.com/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What are the problem?