VB.NETでも独自の等値演算子を実装したい
はじめに
前回の記事を書いていた時私は勘違いをしていました。
正直今まで真面目にVB.NETでオブジェクト指向プログラミンをした事がなく、そうなると真面目にクラスを実装したことがありませんでした。
いろいろとその場のノリとか惰性的な何かがありまして=
とIs
の区別をはっきりさせてませんでした。
=
は値が等価か調べる演算子で別途演算子をオーバーロードする必要があり、対してIs
は参照が等価か調べる演算子って事をさっき知りました。
Is 演算子 (Visual Basic) | Microsoft Docs
これが何かと言いますと、この特性によりC#とVB.NETでは等値演算子を実装するときにほんの少しだけやり方が変わると言うわけです。
実装
Module Module1 Sub Main() Dim a = New Fraction(1, 2) Dim b = New Fraction(1, 2) Console.WriteLine(a Is b) Console.WriteLine(a.Equals(b)) Console.WriteLine(a = b) Console.WriteLine(a <> b) End Sub End Module Class Fraction Dim _numerator As Integer Dim _denominator As Integer Public Sub New(numerator As Integer, denominator As Integer) _numerator = numerator _denominator = denominator End Sub Public Overrides Function Equals(obj As Object) As Boolean If obj Is Nothing OrElse Me.GetType() <> obj.GetType() Then Return False End If Dim other = DirectCast(obj, Fraction) Return Me._numerator = other._numerator AndAlso Me._denominator = other._denominator End Function Public Overrides Function GetHashCode() As Integer Return _numerator Xor _denominator End Function Public Overloads Shared Function Equals( a As Fraction, b As Fraction) As Boolean If a Is b Then Return True If a Is Nothing OrElse b Is Nothing Then Return False Return a.Equals(b) End Function Public Shared Operator =(a As Fraction, b As Fraction) As Boolean Return Equals(a, b) End Operator Public Shared Operator <>(a As Fraction, b As Fraction) As Boolean Return Not a = b End Operator End Class
とりあえず、我らが啓典msdnの当該項目を見てみましょう。
Equals および等値演算子 (==) 実装のガイドライン
正直一部日本語訳が怪しいですが、マイクロソフトへの忠誠心でカバーしましょう。
まあ、要約してしまうと
Equal
とGetHashCode
はセットで実装しろ。でなければハッシュを使うコードの挙動がおかしくなる。IComparable
を実装するときは必ずEquals
を実装しろ。大小の比較が出来るのに等値の比較ができないのはおかしいだろう。IComparable
を実装するときは各比較演算子(=
とか>=
とか)を実装する事を検討しよう。Equals
とかは例外をスローするな。Equals
と=
とで挙動を変えたら殺す
ということです。
別にどう実装しようと開発者の自由ですが、普通の利用者はEquals
と=
で同じ動作することを期待します。
Object.Equals メソッド (Object) (System)
値として扱われるようなものでなければ参照型で等価演算子を実装しないほうが良いと言うことらしいので、今回は値っぽく形だけの分数を表すクラスで等値演算子を実装してみました。 分母と分子を表すフィールドを持ち、どちらも等しい場合に値として等しいと判断させます。 なお、今回は等値演算子を試しに実装するのがメインなので真面目に分数クラスは実装しません。*1
今回はSystem.String
での実装を参考にしながら実装してみました。
Equals(Object)
(と、今回は必要ないけど一応GetHashCode()
)の実装Equals(Object)
を利用してEquals(Fraction, Fraction)
の実装Equals(Fraction, Fraction)
を利用してOperator =(Fraction, Fraction)
を実装Operator =(Fraction, Fraction)
を利用してOperator <>(a As Fraction, b As Fraction)
を実装
の流れでいきます。というよりいきました。もうコード載せてるもん。
今回はNothing = Nothing
はTrue
とします。異論は認めない。
C#での等値演算子実装
ところで同じことをやるとC#では問題があるそうで。
C#では参照を比較するときにはhoge == null
などと書きます。
また、オーバーロードした等値演算子を使うためにもhoge == "Hello World"
と書きます。
オーバーロードした等値演算子の実装の中でそのクラスの型の参照を比較するときに==
を使用するとオーバーロードされた等値演算子が呼び出されて無限に自分自身が呼び出され続けてスタックオーバーフローで死ぬそうです。
これを回避するために一旦Object
にキャストしてから==
で比較したりObject.ReferenceEquals(Object, Object)
を使う必要があります。
Object.ReferenceEquals メソッド (Object, Object) (System)
ところがVB.NETでは参照を比較するときはIs
演算子なのでこの問題そのものが存在しない。
というわけでポンと実装。
using System; namespace OperatorOverload2 { class Program { static void Main(string[] args) { var a = new Fraction(1, 2); var b = new Fraction(1, 2); Console.WriteLine(a.Equals(b)); Console.WriteLine(a == b); Console.WriteLine(a != b); Console.ReadLine(); } } class Fraction { private int _numerator; private int _denominator; public Fraction(int numerator, int denominator) { _numerator = numerator; _denominator = denominator; } public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; var other = (Fraction)obj; return this._numerator == other._numerator && this._denominator == other._denominator; } public override int GetHashCode() { return _numerator ^ _denominator; } public static Boolean Equals(Fraction a, Fraction b) { if (object.ReferenceEquals(a, b)) return true; if (object.ReferenceEquals(a, null) || object.ReferenceEquals(b, null)) return false; return a.Equals(b); // こうすると無限に自分自身を呼び出し続け死ぬ //if (a == b) return true; //if (a == null ||b == null) return false; //return a.Equals(b); } public static bool operator ==(Fraction a, Fraction b) { return Equals(a, b); } public static bool operator !=(Fraction a, Fraction b) { return !(a == b); } } }
Equals(Fraction, Fraction)
のコメントアウトされている部分で実装すると無限再帰の陥って最終的にコールスタックを消費しきって死にます。
おわり
*1:探せばすでにありそう