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