2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-20

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

2
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?