読者です 読者をやめる 読者になる 読者になる

VB.NETでも独自の等値演算子を実装したい

VB.NET

謝意二ング

all work and no play makes jack a dull boy

ではなく、前回の記事を書いていた時私はとんでもない勘違いをしていました。

正直今まで真面目にVB.NETオブジェクト指向プログラミンをした事がなく、そうなると真面目にクラスを実装したことがありませんでした。

いろいろとその場のノリとか惰性的な何かがありまして=Isの区別をはっきりさせてませんでした。 =は値が等価か調べる演算子で別途演算子オーバーロードする必要があり、対してIsは参照が等価か調べる演算子って事をさっき知りました。

Is 演算子 (Visual Basic)

これが何かと言いますと、この特性により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 および等値演算子 (==) 実装のガイドライン

正直一部日本語訳が怪しいですが、マイクロソフトへの忠誠心でカバーしましょう。

まあ、要約してしまうと

  • EqualGetHashCodeはセットで実装しろ。でなければハッシュを使うコードの挙動がおかしくなる。
  • IComparableを実装するときは必ずEqualsを実装しろ。大小の比較が出来るのに等値の比較ができないのはおかしいだろう。
  • IComparableを実装するときは各比較演算子=とか>=とか)を実装する事を検討しよう。
  • Equalsとかは例外をスローするな。
  • Equals=とで挙動を変えたら殺す

ということです。 別にどう実装しようと開発者の自由ですが、普通の利用者はEquals=で同じ動作することを期待します。

Object.Equals メソッド (Object) (System)

値として扱われるようなものでなければ参照型で等価演算子を実装しないほうが良いと言うことらしいので、今回は値っぽく形だけの分数を表すクラスで等値演算子を実装してみました。 分母と分子を表すフィールドを持ち、どちらも等しい場合に値として等しいと判断させます。 なお、今回は等値演算子を試しに実装するのがメインなので真面目に分数クラスは実装しません。*1

今回はSystem.Stringでの実装を参考にしながら実装してみました。

  1. Equals(Object)(と、今回は必要ないけど一応GetHashCode())の実装
  2. Equals(Object)を利用してEquals(Fraction, Fraction)の実装
  3. Equals(Fraction, Fraction)を利用してOperator =(Fraction, Fraction)を実装
  4. Operator =(Fraction, Fraction)を利用してOperator <>(a As Fraction, b As Fraction)を実装

の流れでいきます。というよりいきました。もうコード載せてるもん。

今回はNothing = NothingTrueとします。異論は認めない。

C Sharpでの等値演算子実装の秘密だにゃん

ところで同じことをやるとC#では問題があるそうで。

C#では参照を比較するときにはhoge == nullなどと書きます。 また、オーバーロードした等値演算子を使うためにもhoge == "Hello World"と書きます。 オーバーロードした等値演算子の実装の中でそのクラスの型の参照を比較するときに==を使用するとオーバーロードされた等値演算子が呼び出されて無限に自分自身が呼び出され続けてスタックオーバーフローで死ぬそうです。

これを回避するために一旦Objectにキャストしてから==で比較したりObject.ReferenceEquals(Object, Object)を使う必要があります。

Object.ReferenceEquals メソッド (System)

ところがVB.NETでは参照を比較するときはIs演算子なのでこの問題そのものが存在しない。 いやっほいやったぜさすがおれたちのびじゅあるべーしっくどっとねっと*2

というわけでポンと実装。

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:探せばすでにありそう

*2:他の部分で嫌という程嫌な気分を味わっているわけだが