助けてくれない? オーバーロードでいっぱいっぱいなんだ

Eric Gunnerson
Microsoft Corporation

June 22, 2001

C# 言語の仕様についてこのコラムを続けていますが、今回は "演算子のオーバーロード" について説明します。演算子のオーバーロード (このコラムでは、特定の場合を除いて以下 "オーバーロード" と呼びます) とは、ユーザー定義型を使用して式を作成する機能を指します。これにより、ユーザー定義型に既定の型と同様の機能を持たせることができます。

たとえば、2 つの数値を足す場合に次のような式を使用するのは一般的で、この式からは 2 つの数値の合計が算出されることは明らかです。

  
    int i = 5;
int sum = i + j;

  

次のように、より複雑な数値を表すユーザー定義型 (Complex) を使用して同様の式を作成できれば、便利だと思いませんか?

  
    Complex i = 5;
Complex sum = i + j;

  

演算子のオーバーロードでは、ユーザー定義型に対して "+" などの演算子をオーバーロードすることができます。オーバーロードを使用しないと、次のようなコードを作成しなければなりません。

  
    Complex i = new Complex(5);
Complex sum = Complex.Add(i, j);

  

このコードは問題なく機能しますが、C# 言語では Complex 型は既定の型のようには機能しません。

オーバーロードを利用すべき状況

"演算子のオーバーロード" 機能について誤解しているプログラマが多く存在するため、この機能の評価はプログラマによって非常に異なります。プラグラマによっては、コードが複雑なプログラムをユーザーが作成するための 1 つの方法というように考え、これをプログラミング言語の一部としては扱わない人もいます。逆に、これをすばらしい機能と考え、頻繁に使用するプログラマも存在します。

これらの異なる見解はいずれも多少は正しいと言えますが、どちらも 100% 正しいわけではありません。確かにオーバーロード機能を使用すると複雑なコードを持つプログラムができあがることがありますが、私の経験から言うと、使用する言語にオーバーロード機能が存在しないとしても、このような複雑なコードができあがることはよくあることです。場合によっては、オーバーロード機能が存在しないと、コードの作成がより難しくなることもあります。

オーバーロード機能を頻繁に使用するプログラマが複雑なコードを作成するというのは事実です。

プログラミング言語にオーバーロード機能を含めることの真意は、クラスまたは構造体を使用するユーザーのために概念的な簡易性を提供することにあります。ユーザーが作成するコードがより見やすくなる場合にのみ、演算子をオーバーロードする価値があります。コードが "見やすく" なる場合であり、決して "短く" なる場合ではないことに注意してください。演算子のオーバーロードを使用したクラスは、ほとんどの場合、結果的にコードが短くなりますが、常に見やすくなるわけではありません。

これをわかりやすく説明するために、オーバーロード機能の例を作ってみました。読者の皆さんは、これらのコードを確認して、どの演算子がオーバーロードされているかと、オーバーロードされている演算子がどのように機能するかを調べてください。

クイズ

1

  
    BigNum n1 = new BigNum("123456789012345");
BigNum n2 = new BigNum("11111");
BigNum sum = n1 + n2;

  

B

  
    Matrix m1 = loadMatrix();
Matrix m2 = loadMatrix();
Matrix result = m1 * m2;

  

iii

  
    DBRow row = query.Execute();
while (!row.Done)
{
   Viewer.Add(row);
   row++;
}

  

IV

  
    Account current = findAccount(idNum);
current += 5;

  

解答とディスカッション

1

これは簡単な問題でしたね。この加算は、既定の型を使用しての加算とほとんど同じです。皆さんは、どのような演算子がどのように使用されているかすぐにお分かりでしょう。これは、演算子のオーバーロードを利用すべきケースです。

B

この例ではマトリックスの乗算が行われます。概念上ではマトリックスの乗算は通常のものとは多少異なりますが、これはうまく定義された演算で、マトリックスの乗算を理解しているユーザーであれば、オーバーロードされた演算子がここで使用されていることに何の疑問も抱かないでしょう。

iii

この例では、データベース内の行間を前方に進むためのインクリメント (++) 演算子がオーバーロードされています。インクリメントの意味や、このようなインクリメントがどのような結果をもたらすかは、データベースの行には関係ありません。

また、これはオーバーロードを利用してもコードが簡単にならない例でもあります。このコードは次のように書かれていたほうが見やすいでしょう。

  
    DBRow row = query.Execute();
while (!row.MoveNext())
{
   Viewer.Add(row);
}

  

IV

何かを従業員に与えるということはどういうことでしょうか? この場合、選択肢は恩典プログラムです。この恩恵プログラムを従業員に与えるということは、その従業員はその恩恵プログラムに参加するということになります。これは、演算子のオーバーロードにはまったく向かない状況です。

ガイドライン

オーバーロードを使用する場合についてのガイドラインはとてもシンプルです。そのような演算を行えるとユーザーが思ったときが、オーバーロードを行うべきときです。

算術演算子のオーバーロード

C# 言語で演算子をオーバーロードするには、演算を実行する関数を指定するだけです。この関数は、実行される演算に関連する型で定義されている必要があります。また、少なくとも 1 つのパラメータがその型である必要があります。これにより、int の加算のオーバーロードや、その他の予期せぬ動作を避けることができます。

オーバーロードのデモを行うために、ベクトルを開発することにしましょう。ベクトルとは、原点から特定の 2 次元の点までの線と考えることができます。数多くのさまざまな演算がベクトル上で実行可能です。次は型のアウトラインです。

  
    struct Vector
{
   float x;
   float y;

   public Vector(float x, float y)
   {
      this.x = x;
      this.y = y;
   }
}

  

ベクトルを役に立つものにするために、ベクトルで次の演算を実行可能にする必要があります。

  1. 長さの取得
  2. ベクトルと数値の乗算 (ベクトル x 数値)
  3. ベクトルと数値の除算 (ベクトル ÷ 数値)
  4. ベクトルどうしの加算
  5. ベクトルどうしの減算
  6. 2 つのベクトルの内積の算出

ここで、我々はこれらの演算をどのように実装するかを考える必要があります。

長さ

ベクトルの長さを取得するために使用すべき演算子というものは存在しないような気がします。長さとは変化しないものなので、プロパティとして使用することにします。

  
       public float Length
   {
      get
      {
         return((float) Math.Sqrt(x * x + y * y));
      }
   }

  

数値によるベクトルの乗算/除算

ベクトルに数値を掛ける乗算は一般的な演算であり、ユーザーに頻繁に利用される演算だと言えます。次のように実装してみましょう。

  
       public static Vector operator*(Vector vector, float multiplier)
   {
      return(new Vector(vector.x * multiplier, vector.y * multiplier));
   }

  

ここで、注目すべきいくつかの点があります。まず、演算子が static 関数であるため、この関数は両方のパラメータを受け取り、結果と共に新しいオブジェクトを返さなければならないことです。この演算子の名前は、単に "オーバーロードされる演算子の前にくる演算子" です。

数値によるベクトルの除算を行うコードも同様です。

ベクトルどうしの加算または減算

これらはベクトルについてとても一般的な演算なので、これらをオーバーロードするという決断は簡単に下すことができます。

  
       public static Vector operator+(Vector vector1, Vector vector2)
   {
      return(new Vector(vector1.x + vector2.x, vector1.y + vector2.y));
   }

  

減算を行うコードの実装も同様です。

内積の算出

2 つのベクトルの内積は、ベクトルで定義された特殊な演算であり、既定の型に似た演算はありません。等式の中では、2 つのベクトル間にドット (・) を記述して内積を示すので、その他の既存の演算子と同じ形式になることはありません。内積について興味深い点は、2 つのベクトルを受け取るが、返すのは 1 つのシンプルな数値であることです。

演算がオーバーロードされているかいないかにかかわらず、ユーザー コードはほぼ同じです。最初の行ではオーバーロードされた演算が使用されており、その他の 2 行ではその代替の演算が示されています。

  
       double v1i = (velocity * center) / (t * t);
   double v1i = Vector.DotProduct(velocity, center) / (t * t);
   double v1i = velocity.DotProduct(center) / (t * t);

  

ここで決断を下すことができます。私が作成したクラスでは、内積の計算を行うために "*" 演算子をオーバーロードしましたが、よく考えるとこれはあまりいい選択ではありませんした。

最初の例では、velocity と center がベクトルであることが明確ではなかったため、内積が実行中の演算であることも明確ではありませんでした (それを使用した例を検索中に気付きました...)。2 番目の例では、演算子が何であるかが明確になっています。我ながらいい例だと思います。

3 番目のコードもわかりやすい例だと思いますが、演算がメンバ関数でなければもっと明確な例になったでしょう。

  
       public static double DotProduct(Vector v1, Vector v2)
   {
      return(v1.x * v2.x + v1.y * v2.y);
   }

  

C# と C++ のオーバーロード

C++ と比較すると、C# でオーバーロード可能な演算子の数ほうが少ないことに気付きます。C# には 2 種類の制限事項があります。まず、メンバ アクセス、メンバ実行 (関数呼び出し)、代入式、および "new" はオーバーロードできません。これは、これらの演算はランタイムに行われるためです。

次に、"&&"、"||"、"?:" などの演算子と、"+=" などの複合代入演算子もオーバーロードできません。これは、演算子がある程度以上に複雑になると、オーバーロードを行う意味自体が失われるためです。

オーバーロードされた変換

最初の例に戻りましょう。

  
    Complex i = 5;
Complex sum = i + j;

  

加算の演算子をオーバーロードする方法はわかりましたが、最初のステートメントを機能させるためにはそれだけでは足りません。まだ何かを行う必要があります。その何かとは、変換をオーバーロードすることです。

暗黙的な変換と明示的な変換

C# 言語では、暗黙的および明示的な変換の両方をサポートしています。暗黙的な変換は、対象 (変換後) の型の範囲がソース (変換前) の型の範囲と同等かそれ以上であるため、常に成功する変換と一般的に言うことができます。short 型から int 型への変換は暗黙的な変換です。暗黙的な変換は、代入式ステートメントの一部として許容されています。

  
       short svalue = 5;
   long lvalue = svalue;

  

明示的な変換では、データが損失したり、例外が発生する可能性があります。このため、明示的な変換ではギブスの役目を果たすもの (丸カッコ) が必要です。

  
       long lvalue = 5;
   short svalue = (short) lvalue;

  

変換をオーバーロードする際は、暗黙的にするか明示的にするかを決めなければなりません。決断を下す際は、"暗黙的な変換モデルは安全"、"明示的な変換モデルには危険が伴う" ということを覚えておいてください。

整数値 (5) から Complex 値への変換は次のように定義できます。

  
       public static implicit operator Complex(int value)
   {
      return(new Complex(value, 1.0));
   }

  

この例では、int (整数型) から Complex 型への暗黙的な変換が行われます。

言語間の相互運用性

ここまでは、C# 言語内における演算子のオーバーロードについて説明してきました。その他の言語を併用する場合は複雑度が多少増すことになります。

演算子のオーバーロードは .NET Common Language Subset の機能ではありません。これは、オーバーロードはすべての言語では利用できないということを意味します。そのため、オーバーロードを使用しないコードも併せて提供し、その他の言語でも同じ動作を実行できるようにすることが大切です。たとえば、クラスで加算の演算子が定義されている場合は、同じようなコードに "Add" などと名前を付けて、同様の動作を実行するように定義することをお勧めします。

お勧め Web サイト

今回紹介するのは次の Web サイトです。

http://www.cshrp.net/ Leave-ms (英語)

Eric Gunnerson は、C# コンパイラ チームの QA 担当の責任者で、C# デザイン チームのメンバでもあり、『A Programmer's Introduction to C#』の著者でもあります。 彼は、8 インチのフロッピー ディスクが何であるかを知っているぐらい昔からプログラミングを行っています。さらに、かつては簡単にテープを装着できました。