連載

C#入門 最終回

第22回 落ち穂拾い

(株)ピーデー
川俣 晶
2002/03/01


演算子を定義する

 C#では、C++などと同様に、例えば加算を表す「+」などの演算子の機能を自分で定義することができる。例えば複素数や行列など、単純な実数型では表現しきれないときに、この機能を使って、それらを一種の数値として演算できるクラスを作成すると、プログラム・コードが見通しやすくなる。演算子の機能を定義した例は以下のとおりである。

 1: using System;
 2:
 3: namespace ConsoleApplication5
 4: {
 5:   class Class2
 6:   {
 7:     public string str;
 8:     public Class2( string s ) { str = s; }
 9:     public static Class2 operator -(Class2 x)
10:     {
11:       return new Class2( "-" + x.str );
12:     }
13:     public static Class2 operator +(Class2 x, Class2 y)
14:     {
15:       return new Class2( x.str + "+" + y.str );
16:     }
17:   }
18:   class Class1
19:   {
20:     static void Main(string[] args)
21:     {
22:       Class2 a = new Class2("Hello");
23:       Class2 b = new Class2("World");
24:       Class2 c = a + b;
25:       Console.WriteLine(c.str);
26:       Class2 d = -c;
27:       Console.WriteLine(d.str);
28:     }
29:   }
30: }
演算子の機能を定義したサンプル・プログラム5
Class2クラスでoperatorキーワードを用いて+演算子と−演算子を定義している。

 これを実行すると以下のようになる。

サンプル・プログラム5の実行結果
定義した+演算子は2つの文字列を“+”記号を用いて連結し、−演算子は文字列の先頭に“-”記号を付け加える。

 この例では、メンバ変数のstrの先頭に“-”記号を付け加える−演算子(9〜12行目)と、2つのインスタンスの持つ文字列を“+”記号を介して連結する+演算子(13〜16行目)を定義している。演算子の定義はメソッドの定義と似ているが、メソッド名の代わりにoperatorキーワードを用いて、“operator -”のように演算子の名前を指定する点が異なる。引数は単項演算子なら1個。2項演算子なら2個を用意する。戻り値の型としては、その演算の結果として得られる型を記述する。そして、演算子はインスタンスから独立した存在なので、staticキーワードを付けておく。

 例えば13行目は、Class2型の2つの値を得て、Class2型の値を返すことを宣言している。実際に実行される内容は15行目に記述されているが、見てのとおり、普通のメソッドと何ら変わることはない。しかし、これを実際に呼び出す側のソースはメソッド呼び出しとはまったく違って見える。それが24行目だ。まるで整数を2つ足し合わせているように簡単に記述されているが、これが、13〜16行目のコードを呼び出しているのである。同様に、26行目のコードが、9〜12行目のコードを呼び出している。

 演算子の定義を使うと劇的にソースが読みやすくなる事例があるのは事実である。例えば、座標を保持するSystem.Drawing.Pointには、+や−の演算子が存在するが、座標値というのはもともと足したり引いたりするものなので、これらの演算子を使いこなせばソースが分かりやすくなるだろう。しかし、もともと計算になじまない役割を与えられたクラスに対して、無理に+や−の演算子を付けても、ソース・コードはかえって分かりにくくなる。多用すればよいというわけではない点に注意しよう。

as演算子

 データ型を変換するas演算子というものがC#にはある。機能としては、キャストによく似た役割を持つ。キャストと異なるのは、変換不能の事態に直面したとき、例外を投げるのではなく、nullを返すことと、ユーザー定義の変換を扱わないことである。以下はas演算子をキャストと比較しながら使用した例である。

 1: using System;
 2:
 3: namespace ConsoleApplication6
 4: {
 5:   class Class2
 6:   {
 7:   }
 8:   class Class3 : Class2
 9:   {
10:   }
11:   class Class1
12:   {
13:     static void Main(string[] args)
14:     {
15:       object instance1 = new Class3();
16:       Console.WriteLine(instance1);
17:       Class2 instance2 = instance1 as Class2;
18:       Console.WriteLine(instance2);
19:       Class2 instance3 = (Class2)instance1;
20:       Console.WriteLine(instance3);
21:       Class1 instance4 = instance1 as Class1;
22:       Console.WriteLine(instance4);
23:       Class1 instance5 = (Class1)instance1;
24:       Console.WriteLine(instance5);
25:     }
26:   }
27: }
as演算子をキャストと比較するためのサンプル・プログラム6
Class3のインスタンスをClass1に変換することはできない。このプログラムを実行すると、23行目のキャスト部分で例外がスローされる。

 これを実行すると以下のようになる。

サンプル・プログラム6の実行結果
実行結果の最初の3行はインスタンスの型が表示されている。そのすぐ下の行はデータ型の変換に失敗するため空行となるが、例外はスローされていない。

 結果の画面写真を見ると、22行目の出力結果が出ていないように思うかもしれないが、表示する内容がnullのため、空行が表示されているのである。23行目のキャストでは同じ変換を行っているが、これは例外をスローしている。変換成功の場合は、キャストもas演算子も同じように機能しているが、変換できない場合の挙動が同じではないことが見て取れると思う。

スコープと変数

 プログラムの中に同じ名前の変数がいくつも登場することは、決して珍しくない。まったく独立して混乱をきたさない文脈で、同じ名前の変数を別個に宣言することは、コンパイラを混乱させることはなく、あいまいさなく実行コードを生成することができる。しかし時として、コンパイラは混乱しなくてもソースを読み書きする人間が混乱する場合もある。そこでC#では、同じ名前の変数の使用を強制的に制限する機能を持っている。以下はその機能を調べるために作成してみたソース・コードである。コンパイル・エラーを見るためのソースなので、実行することはできない。

 1: using System;
 2:
 3: namespace ConsoleApplication7
 4: {
 5:   class Class1
 6:   {
 7:     private static int x=0;
 8:     static void sample1()
 9:     {
10:       x = 1;
11:       int x=2;  // ローカルの変数 'x' をこのスコープで宣言することはできません。これは、'親またはカレント' スコープで別の意味を持つ 'x' の意味が変更されるのを避けるためです。
12:     }
13:     static void sample2()
14:     {
15:       int x=1;
16:       x = 2;
17:     }
18:     static void Main(string[] args)
19:     {
20:       if( args[0] == "a" )
21:       {
22:         int x=1;
23:         if( x == 1 )
24:         {
25:           int x=2;  // ローカルの変数 'x' をこのスコープで宣言することはできません。これは、'親またはカレント' スコープで別の意味を持つ 'x' の意味が変更されるのを避けるためです。
26:         }
27:       }
28:       else if( x == 1 )   // 'x' は宣言 'ConsoleApplication7.Class1.x' と競合しています。
29:       {
30:         int x=3;
31:       }
32:     }
33:   }
34: }
変数のスコープを調べるためのサンプル・プログラム7
このプログラムはコンパイル・エラーを確認するためのものなので、実行はできない。

 このソースには、クラスClass1に“x”という名前のメンバ変数が存在する。そして、それぞれのメソッド内部で、あらためて“x”というローカル変数を宣言することを試みている。まず8〜12行目のメソッドsample1と、13〜17行目のメソッドsample2を比較していただきたい。sample1は、まずクラスのメンバ変数xにアクセスした後で、ローカル変数xを宣言しようとしている。1つのブロックの中で、同じxという名前を別の変数を示すために使用することは許されておらず、コンパイル・エラーが発生する。しかし、sample2の方は、15行目も16行目も同じ1つのxを扱っているため、これはエラーにならない。25行目は、21〜27行目の親スコープ内でxという変数が宣言されている状態で、24〜26行目の子スコープで同じ変数名を宣言しようとしたものである。この場合、子スコープ内で異なる変数を同じ内容で参照しようとしたわけではないがエラーになる。28行目は、クラスのメンバ変数xを参照しようとしているが、このメソッド内ではすでにxというローカル変数が宣言されておりエラーになる。

 このエラーに遭遇した場合、変数名を変更することが必要となる。変数名を、区別できる分かりやすい名前に書き換えることは、ソースを分かりやすくすることにも貢献するので、ぜひ行うべきだろう。


 INDEX
  第22回 落ち穂拾い
    1.コンポーネントという言葉の意味
    2.ユーザー定義のデータ型変換
  3.演算子を定義する
    4.constとreadonly
    5.シールされたクラス
 
「C#入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間