演算子のオーバーロード

目次

キーワード

概要

オブジェクト指向言語ではクラスを定義することで自分の思い通りの「型」を作ることが出来ます。 このような自作の型は、intdouble などの組込み型と区別するため、 ユーザー定義型と呼ばれています。 ユーザー定義型の理想は、組込み型とまったく同じように扱えることです。

ユーザー定義型をあたかも組込み型であるかのように扱えるようにするため、 C#には演算子のオーバーロードというものが用意されています。 C#の組込み型には +- などの演算子が用意されていますが、 演算子のオーバーロードを行うことで、 ユーザー定義型にも自分で演算子を定義することが出来、 組込み型と同じように扱うことができます。

演算子のオーバーロードの方法

演算子は operator キーワードを用いることで、 クラスの静的メソッドとして以下のようにして定義することが出来ます。

public static 戻り値の型 operator演算子 (引数リスト)

例えば、これまでに例としてあげてきた複素数クラスに加算演算子 + を定義したい場合、 以下のように書きます。

class Complex
{
  public static Complex operator+ (Complex z, Complex w)
  {
    return new Complex(z.Re + w.Re, z.Im + w.Im);
  }
  // 残りの部分は省略
}

演算子の定義は必ず public で static にする必要があります。 また、戻り値の型は、演算子を定義するクラスから暗黙的に変換できるクラスである必要があります。 例えば、以下のようにすると、コンパイル時に 「error CS0029: 型 'Complex' を型 'double' に暗黙的に変換できません。」 というエラーメッセージが表示されます。

class Complex
{
  // Complexからdouble型に暗黙的に変換できないのでエラーになる。
  //              ↓
  public static double operator+ (Complex z, Complex w)
  {
    return new Complex(z.Re + w.Re, z.Im + w.Im);
  }
  // 残りの部分は省略
}

引数リストは、 +, -, *, / などの2項演算子なら2つ、 ++, --, !, ~ などの単項演算子なら1つの引数を指定します。 また、引数のうち少なくとも1つの型は演算子を定義するクラス自身である必要があります。

class Complex
{
  // ↓この2つはOK。
  public static Complex operator+ (Complex z, double w){return new Complex(z.Re + w, z.Im);}
  public static Complex operator+ (double z, Complex w){return new Complex(z + w.Re, w.Im);}

  // ↓エラー。引数の少なくともどちらか一方は Complex でないと駄目。
  public static Complex operator+ (double z, double w){return new Complex(z + w, 0);}

  // 残りの部分は省略
}

オーバーロード可能な演算子

演算子の一覧とオーバーロード可能かどうかを以下に示します。

演算子オーバーロード可能かどうか
+, -, !, ~, ++, --, true, falseこれらの単項演算子はオーバーロード可能です。
+, -, *, /, %, &, |, ^, <<, >>これらの2項演算子はオーバーロード可能です。
==, !=, <, >, <=, >=これらの比較演算子はオーバーロード可能です。
&&, || これらの条件 AND/OR 演算子は直接オーバーロードすることは出来ませんが、 &, |, true, false をオーバーロードすることで利用可能になります。
[] 配列の添字演算子はインデクサとして定義することが出来ます。 詳しくは 「インデクサ」 で説明します。
キャスト キャストは型変換演算子として定義することが出来ます。
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= これらの代入演算子は直接オーバーロードすることは出来ませんが、 対応する2項演算子をオーバーロードすることで利用可能になります。
=, ., ?:, ->, new, is, sizeof, typeofこれらの演算子はオーバーロード出来ません。

+ などの演算子は特に説明は必要ないと思います。 ここでは、説明の必要になりそうな演算子のみをとりあげます。

true, false 演算子

true, false 演算子が定義された型のオブジェクトは ifwhile, for, ?: などで条件式として利用することが出来ます。

true, false 演算子のどちらか一方を定義する場合、もう一方も定義する必要があります。 また、true, false 演算子の戻り値の型は bool でなければなりません。

class Bool
{
  int i;
  public Bool(int i){this.i = i;}
  public static bool operator true(Bool b){return b.i != 0;}
  public static bool operator false(Bool b){return b.i == 0;}
}

class OperatorSample
{
  public static void Main()
  {
    Bool b = new Bool(0);

    if(b) // 条件式として利用できる
      Console.Write("b == true");
    else
      Console.Write("b == false");
  }
}
b==false

インクリメント・デクリメント

インクリメント・デクリメント演算子は一度インスタンスをコピーし、 コピー後のインスタンスの値を変更し、戻り値とします。 前置き(++x)と後置き(x++)の2つの形式がありますが、 それぞれ以下のような手順で呼び出されます。

前置き

後置き

class Counter
{
  int i;
  public Counter(int i){this.i = i;}
  public static Counter operator ++(Counter c)
  {
    // c を直接書き換えては駄目。
    // インスタンスのコピーを作る。。
    Counter tmp = new Counter(c.i + 1);
    return tmp;
  }
  public override string ToString(){return this.i.ToString();}
}

class OperatorSample
{
  public static void Main()
  {
    Counter c = new Counter(0);

    Console.Write(c++ + "\n"); // Counter tmp = c; c = Counter.operator++(c); return tmp;
    Console.Write(c   + "\n");
    Console.Write(++c + "\n"); // c = Counter.operator++(c); return c;
    Console.Write(c   + "\n");
  }
}
0
1
2
2

条件 AND/OR 演算子

&&, || 演算子は直接オーバーロードすることは出来ませんが、 &, | 演算子および true, false 演算子をオーバーロードすることで利用可能になります。

T 型の変数 x, y に対して、 x && yT.operator false(x) ? x : T.operator &(x, y) として評価されます。 すなわち、xfalse として評価された場合、y は評価されません。

同様に、 x || yT.operator true(x) ? x : T.operator |(x, y) として評価されます。

class Bool
{
  int i;
  public Bool(int i){this.i = i==0 ? 0 : 1;}
  public static bool operator true(Bool b)
  {
    Console.Write("  operator true called\n");
    return b.i != 0;
  }
  public static bool operator false(Bool b)
  {
    Console.Write("  operator false called\n");
    return b.i == 0;
  }
  public static Bool operator &(Bool a, Bool b)
  {
    Console.Write("  operator & called\n");
    return new Bool(a.i & b.i);
  }
  public static Bool operator |(Bool a, Bool b)
  {
    Console.Write("  operator | called\n");
    return new Bool(a.i | b.i);
  }
}

class OperatorSample
{
  public static void Main()
  {
    Bool a = new Bool(1);
    Bool b = new Bool(0);

    Bool c;
    Console.Write("a && b\n");
    c = a && b;
    Console.Write("b && a\n");
    c = b && a;
    Console.Write("a || b\n");
    c = a || b;
    Console.Write("b || a\n");
    c = b || a;
  }
}
a && b
  operator false called
  operator & called
b && a
  operator false called
a || b
  operator true called
b || a
  operator true called
  operator | called

代入演算

代入演算子は直接オーバーロードすることは出来ませんが、 対応する2項演算子をオーバーロードすることで利用可能になります。

例えば、+ 演算子をオーバーロードした型は、 x += y とすることで、 x = x + y と同じ結果が得られます。

型変換演算

型変換(cast)演算子は以下のようにして定義します。

public static explicitまたはimplicit operator 変換先の型 (変換元の型 引数名)
{
  // 変換コード
}

explicit を指定して型変換演算子を定義した場合、 明示的にキャストを行わなければ型変換を行いません (明示的型変換)。 一方、 implicit を指定して型変換演算子を定義した場合、 型変換が必要になった時に自動的に型変換を行います (暗黙的型変換)。

implicit を指定した場合、 意図しないところで勝手に型変換が行われてしまう可能性があるので、 出来る限り explicit を指定しましょう。

また、変換先の型と変換元の型の少なくともどちらか一方は型変換演算子を定義するクラス自身である必要があります。

using System;

class Counter
{
  int i;

  public Counter(int i){this.i=i;}

  public static explicit operator Counter (int i){return new Counter(i);}
  public static explicit operator int (Counter c){return c.i;}
  public override string ToString(){return "count="+this.i;}
}

class OperatorSample
{
  public static void Main()
  {
    Counter c = new Counter(1);
    Console.Write((int)c + "\n");
    Console.Write((Counter)2 + "\n");
  }
}
1
count=2