演算子のオーバーロード

演算子の「上書き」

クラスのインスタンスはただの参照型の変数なので、インスタンス同士で足し算などはできません。


class MyPoint
{
    public int X;
    public int Y;

    public MyPoint() { }
    public MyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

static void Main(string[] args)
{
    MyPoint p1 = new MyPoint(1, 2);
    MyPoint p2 = new MyPoint(3, 4);

    //エラー
    MyPoint p3 = p1 + p2;
}      

しかし上記のようなコードが書けるといろいろと便利です。
そのためには演算子のオーバーロードを行います。


class MyPoint
{
    public int X;
    public int Y;

    public MyPoint() { }
    public MyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    //+演算子のオーバーロード
    public static MyPoint operator +(MyPoint p1, MyPoint p2)
    {
        //ふたつのインスタンスの
        //フィールドXとフィールドYを加算した
        //新しいインスタンスを生成して返す
        return new MyPoint(p1.X + p2.X, p1.Y + p2.Y);
    }
}

static void Main(string[] args)
{
    MyPoint p1 = new MyPoint(1, 2);
    MyPoint p2 = new MyPoint(3, 4);

    MyPoint p3 = p1 + p2;

    Console.WriteLine("X={0}, Y={1}", p3.X, p3.Y);

    Console.ReadLine();
}
X=4, Y=6

演算子のオーバーロードは、この例のようにそのままでは使用できない演算子を使用できるようにするほか、使用はできるけれどもよりクラスの用途に沿った処理に置き換える目的で行われます。

ただし本来の意味を変えてしまうようなオーバーロードは避けるべきです。
(+演算子で掛け算をするとか)

演算子のオーバーロードの書式

演算子のオーバーロードはユーザー定義クラス内で以下の書式で行います。


public static 戻り値の型 operator 演算子(引数リスト)
{
    //変換処理
}

publicstaticは必須です。
戻り値の型はオーバーロードする演算子の種類によって異なります。
比較演算子とtruefalse演算子の場合はbool型、それ以外の演算子の場合は自由です。

引数の数は、オーバーロードする演算子が単項演算子ならばひとつ、二項演算子ならふたつです。
例えば二項+演算子は計算に値がふたつ必要なので、引数もふたつ必要となります。
データ型は少なくとも一つは演算子オーバーロードを定義しようとしている自作クラスである必要があります。

単項演算子とは例えば「-2」のマイナス記号のように、値がひとつだけで使用できる演算子です。
二項演算子は「3 - 2」のマイナス記号ように、演算に値をふたつ(左辺と右辺)を使用する演算子です。

+-演算子などは単項と二項の両方が存在するので、どちらをオーバーロードするかは引数の数で識別されます。
両方を定義したい場合は以下のようになります。


class MyPoint
{
    public int X;
    public int Y;

    public MyPoint() { }
    public MyPoint(MyPoint p)
        : this(p.X, p.Y) { }
    public MyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    //単項+演算子のオーバーロード
    public static MyPoint operator +(MyPoint p)
    {
        return new MyPoint(p);
    }
    
    //二項+演算子のオーバーロード
    public static MyPoint operator +(MyPoint p1, MyPoint p2)
    {
        return new MyPoint(p1.X + p2.X, p1.Y + p2.Y);
    }
}

オーバーロードが可能な演算子は以下です。

単項演算子 +、-、!、~、++、--、true、false
二項演算子 +、-、*、/、%、&、|、^、<<、>>
比較演算子 ==、!=、<、>、<=、>=

+=/=などの複合代入演算子は明示的なオーバーロードはできませんが、+/の二項演算子をオーバーロードすると使用できるようになります。

他の型との演算

+/などの二項演算子は、他のデータ型との演算も定義可能です。


private class SimpleClass
{
    string name;

    public SimpleClass(string s)
    {
        name = s;
    }

    //自クラス + 自クラス
    public static SimpleClass operator +(SimpleClass sc1, SimpleClass sc2)
    {
        return new SimpleClass(sc1.name + sc2.name);
    }

    //自クラス + string型
    public static SimpleClass operator +(SimpleClass sc, string s)
    {
        return new SimpleClass(sc.name + s);
    }

    //string型 + 自クラス
    public static SimpleClass operator +(string s, SimpleClass sc)
    {
        return new SimpleClass(s + sc.name);
    }
}

このサンプルコードでは、「自クラス+自クラス」「自クラス+string型」「string型+自クラス」の三パターンの演算が可能になります。

自クラスのインスタンスは必ずひとつは引数に取る必要があるので、「string型+string型」のようなものは定義できません。

等値演算子、非等値演算子

等値演算子(==)をオーバーロードする場合は非等値演算子(!=)もオーバーロードする必要があります。
(逆も然り)


private class SimpleClass
{
    int id;
    string name;

    public SimpleClass(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    public static bool operator ==(SimpleClass sc1, SimpleClass sc2)
    {
        if (sc1 == null || sc2 == null)
            return false;
        return sc1.id == sc2.id && sc1.name == sc2.name;
    }

    //上で定義した等値演算子を利用して非等値演算子の処理を定義する
    public static bool operator !=(SimpleClass sc1, SimpleClass sc2)
    {
        return !(sc1 == sc2);
    }
}

まず等値演算子をオーバーロードし、それを利用して非等値演算子をオーバーロードします。
こうすると個別に処理を書くより簡単ですし間違いがありません。

Equals、GetHashCodeのオーバーライド

等値演算子をオーバーロードする場合、EqualsGetHashCodeと言うメソッドをオーバーライドすることが推奨されます。
これらは必須ではありませんが、基本的にEqualsメソッドは等値演算子と同じ働きをします。
等値演算子の挙動を変えるならばEqualsメソッドも同じ動作となるように変更するのが好ましいです。
GetHashCodeメソッドはDictionaryクラスなどで利用されていて、これもキーの一致の判定に使用されるので挙動が異なるとうまく動作しなくなる恐れがあります。

C#ではすべてのデータ型はObject型から派生しています。
EqualsGetHashCodeメソッドはObject型で定義されています。
これらを自作クラスでオーバーライドしない場合、Object型のEqualsGetHashCodeメソッドが使用されます。


private class SimpleClass
{
    int id;
    string name;

    public SimpleClass(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    public static bool operator ==(SimpleClass sc1, SimpleClass sc2)
    {
        if (sc1 == null || sc2 == null)
            return false;
        return sc1.id == sc2.id && sc1.name == sc2.name;
    }

    public static bool operator !=(SimpleClass sc1, SimpleClass sc2)
    {
        return !(sc1 == sc2);
    }

    public override bool Equals(object obj)
    {
        if (obj is SimpleClass)
            return this == (SimpleClass)obj;
        return false;
    }

    public override int GetHashCode()
    {
        return id ^ name.GetHashCode();
    }
}

Equalsメソッドは二つのオブジェクトが等しいか否かを返します。
等値演算子と同じですが、引数がobject型なのでまず自クラスか否かをチェックしてから比較します。

GetHashCodeメソッドはハッシュ値という値を取得するメソッドです。
ハッシュ値は、同じデータを持つオブジェクトであれば常に同じ値を返さなければなりません。
異なるデータ同士であれば基本的には異なるハッシュ値を返しますが、同じハッシュ値を返したとしても問題ありません。
極論を言えば全ての場合で同じ値を返しても良いわけですが、ハッシュ値を利用する処理の効率が悪くなります。
ここでは全てのフィールドのハッシュ値を^演算子(XOR)でビット演算したものを返します。
組み込み型(string型など)はGetHashCodeメソッドを持っているので、これを利用します。
int型もGetHashCodeメソッドを持っていますが、元が単なる整数値なのでこれをそのまま利用してもかまいません。

true、false演算子

truefalse演算子をオーバーロードすると、インスタンスをif文などの条件式に指定できます。
true演算子をオーバーロードした場合はfalse演算子もオーバーロードする必要があります。
(逆も然り)


private class SimpleClass
{
    int id;
    string name;

    public SimpleClass(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    public static bool operator true(SimpleClass sc)
    {
        //idが0の場合は偽とする
        return sc.id != 0;
    }
    public static bool operator false(SimpleClass sc)
    {
        return sc.id == 0;
    }
}

static void Main(string[] args)
{
    SimpleClass sc1 = new SimpleClass(0, "aaa");
    SimpleClass sc2 = new SimpleClass(1, "bbb");

    if (sc1)
        Console.WriteLine("sc1はtrue");
    else
        Console.WriteLine("sc1はfalse");

    if (sc2)
        Console.WriteLine("sc2はtrue");
    else
        Console.WriteLine("sc2はfalse");

    Console.ReadLine();
}
sc1はfalse
sc2はtrue

キャスト演算子のオーバーロード

型変換のためのキャスト演算子もオーバーロード可能です。
キャスト演算子のオーバーロードは以下のいずれかの形式で行います。


public static explicit operator 変換先の型(変換元の型 引数名)
{
    //変換処理
}

public static implicit operator 変換先の型(変換元の型 引数名)
{
    //変換処理
}

explicitキーワードでのオーバーロードの場合は明示的な型変換が必要です
implicitキーワードでのオーバーロードの場合は暗黙的な型変換が可能です。

暗黙的な型変換はプログラマが意図しないところで変換が行われるおそれがあるので、explicitキーワードでのオーバーロードの方が良いでしょう。


class SimpleClass
{
    public int num;

    public SimpleClass(int n)
    {
        num = n;
    }

    //SimpleClassをintに変換する
    public static explicit operator SimpleClass(int n)
    {
        return new SimpleClass(n);
    }

    //intをSimpleClassに変換する
    public static explicit operator int(SimpleClass sc)
    {
        return sc.num;
    }
}

static void Main(string[] args)
{
    int num1 = 5;

    SimpleClass sc = (SimpleClass)num1;
    int num2 = (int)sc;

    Console.WriteLine(sc.num);
    Console.WriteLine(num2);

    Console.ReadLine();
}
5
5

なお、キャスト演算子はオーバーロードが可能ですが、as演算子のオーバーロードはできません。