目次

キーワード

概要

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

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

このように、演算子のオーバーロードによってユーザ定義型に追加された演算子のことをユーザ定義演算子と呼びます。

ポイント
  • 組み込み型(int や string など)とユーザー定義型(クラスや構造体)の区別をなくそう。

  • ユーザー定義型にも、組み込み型と同じように+-などの演算子が定義できます。

  • 書き方は、T operator+ (T x, T y) { ... }

注意: 乱用禁止

演算子のオーバーロードの最大の目的は、ユーザー定義型と組み込み型の差をなくすことです。

逆に言うと、オーバーロードした演算子は、組み込み型と似たような挙動をすべきです。 + 演算子なら加算、 > 演算子なら大なり比較というように、 元の意味と同じ、あるいは、少なくとも似ている操作であるべきです。 この範囲を超えての乱用は避けるべきでしょう。

このように考えると、演算子のオーバーロードが有用な場面は限られます。 かろうじて、+ 演算子は文字列やデリゲートなど、結合にも使われるので用途も広がります。 しかし、他の演算子に関しては、複素数型のような数値を表す型など、ごく限られた型でしかまず使いません。

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

演算子は 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 にする必要があります。

引数リストは、 +, -, *, / などの2項演算子なら2つ、 ++, --, !, ~ などの単項演算子なら1つの引数を指定します。

演算子をオーバーロードできるといっても、C# の文法を変えてしまうようなオーバーロードはできません。 たとえば、2項演算子である / 演算子を単項演算子としてオーバーロードすることはできません。

また、引数のうち少なくとも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 演算子が定義された型のオブジェクトは 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
{
  static void Main()
  {
    Bool b = new Bool(0);

    if(b) // 条件式として利用できる
      Console.Write("b == true");
    else
      Console.Write("b == false");
  }
}
b==false
インクリメント・デクリメント

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

前置き

  • x を引数として++, --演算子を呼び出し、その結果を x に代入する。

  • x をそのまま戻り値として返す。

後置き

  • x を別の場所に保存する。

  • x を引数として++, --演算子を呼び出し、その結果を x に代入する。

  • 別の場所に保存しておいた、 x の変更前の値を戻り値として返す。

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
{
  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
{
  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
{
  static void Main()
  {
    Counter c = new Counter(1);
    Console.Write((int)c + "\n");
    Console.Write((Counter)2 + "\n");
  }
}
1
count=2

演習問題

問題1

クラス問題 1Point 構造体を2次元ベクトルとみなして、 ベクトルの和・差を計算する演算子 + および - を追加せよ。

/// <summary>
/// ベクトル和
/// </summary>
/// <param name="a">点A</param>
/// <param name="b">点B</param>
/// <returns>和</returns>
public static Point operator +(Point a, Point b)

/// <summary>
/// ベクトル差
/// </summary>
/// <param name="a">点A</param>
/// <param name="b">点B</param>
/// <returns>和</returns>
public static Point operator -(Point a, Point b)

解答例1

using System;

/// <summary>
/// 2次元の点をあらわす構造体
/// </summary>
struct Point
{
  double x; // x 座標
  double y; // y 座標

  #region 初期化

  /// <summary>
  /// 座標値 (x, y) を与えて初期化。
  /// </summary>
  /// <param name="x">x 座標値</param>
  /// <param name="y">y 座標値</param>
  public Point(double x, double y)
  {
    this.x = x;
    this.y = y;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// x 座標。
  /// </summary>
  public double X
  {
    get { return this.x; }
    set { this.x = value; }
  }

  /// <summary>
  /// y 座標。
  /// </summary>
  public double Y
  {
    get { return this.y; }
    set { this.y = value; }
  }

  #endregion
  #region 演算子

  /// <summary>
  /// ベクトル和
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>和</returns>
  public static Point operator +(Point a, Point b)
  {
    return new Point(a.x + b.x, a.y + b.y);
  }

  /// <summary>
  /// ベクトル差
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>和</returns>
  public static Point operator -(Point a, Point b)
  {
    return new Point(a.x - b.x, a.y - b.y);
  }

  #endregion

  /// <summary>
  /// A-B 間の距離を求める。
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>距離AB</returns>
  public static double GetDistance(Point a, Point b)
  {
    double x = a.x - b.x;
    double y = a.y - b.y;
    return Math.Sqrt(x * x + y * y);
  }

  public override string ToString()
  {
    return "(" + x + ", " + y + ")";
  }
}

/// <summary>
/// 2次元空間上の三角形をあらわす構造体
/// </summary>
class Triangle
{
  Point a;
  Point b;
  Point c;

  #region 初期化

  /// <summary>
  /// 3つの頂点の座標を与えて初期化。
  /// </summary>
  /// <param name="a">頂点A</param>
  /// <param name="b">頂点B</param>
  /// <param name="c">頂点C</param>
  public Triangle(Point a, Point b, Point c)
  {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 頂点A。
  /// </summary>
  public Point A
  {
    get { return a; }
    set { a = value; }
  }

  /// <summary>
  /// 頂点B。
  /// </summary>
  public Point B
  {
    get { return b; }
    set { b = value; }
  }

  /// <summary>
  /// 頂点C。
  /// </summary>
  public Point C
  {
    get { return c; }
    set { c = value; }
  }

  #endregion

  /// <summary>
  /// 三角形の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    Point ab = b - a;
    Point ac = c - a;
    return 0.5 * Math.Abs(ab.X * ac.Y - ac.X * ab.Y);
  }

  /// <summary>
  /// 三角形の周の長さを求める。
  /// </summary>
  /// <returns>周</returns>
  public double GetPerimeter()
  {
    double l = Point.GetDistance(this.a, this.b);
    l += Point.GetDistance(this.a, this.c);
    l += Point.GetDistance(this.b, this.c);
    return l;
  }
}

/// <summary>
/// Class1 の概要の説明です。
/// </summary>
class Class1
{
  static void Main()
  {
    Triangle t = new Triangle(
      new Point(0, 0),
      new Point(3, 4),
      new Point(4, 3));

    Console.Write("{0}\n", t.GetArea());
    Console.Write("{0}\n", t.GetPerimeter());
  }
}

更新履歴

ブログ