第 2 章 C# の基礎
Microsoft Visual C# .NETは、C#プログラミング言語を使ってオブジェクト指向のアプリケーションやコンポーネントを開発するための充実した環境を提供します。Visual C# .NETは、Microsoft .NET Frameworkを活用し、独自のクラスの構築に利用できるビルトイン型を豊富に提供します。このようなクラスのサブセットは、他の.NET言語とのやり取りに使用できます。また、Visual C# .NETが提供する広範囲なクラスライブラリは、使用することはもちろん、拡張することも可能です。
C#を使って作成できる基本型には、次のものが含まれます。
- クラス ── ビルトイン型を操作するための新しい型やメソッドを定義する。
- 例外 ── クラスで発生するエラーを説明する。
- インターフェイス ── 関連するメソッドを1つにまとめる。
- 構造体 ── 複数の型から1つの新しい型を作成する。
- 列挙 ── 型に対して名前の付いた値の範囲を定義する。
これらの基本型は、他の言語で提供される機能に似ています。多くの場合、C#はさらに機能を追加することにより、コンポーネントやアプリケーションの開発者がこれらの言語構造を利用しやすくしています。
これらの機能を他の言語で知っている場合は、少なくとも本章の内容にひととおり目を通し、これまでに使用していた言語とC#との違いを理解してください。 |
2.1 | 基本データ型
C#言語は.NET Frameworkと密接に結び付いているため、C#開発者に提供されるビルトインデータ型が.NETプラットフォームの一部であると聞いても、意外ではないでしょう。たとえば、C#にはstringというビルトイン型がありますが、これは.NET Frameworkに含まれているSystem.Stringクラスに相当します。string型は連結などのさまざまな文字列操作処理をサポートします。表2-1に、C#言語の一部であるビルトインデータ型と、それに相当する.NET Frameworkの型を示します。
▼ 表2-1 ビルトインのC#の型と対応する.NET Frameworkの型
C#の型 | .NET Frameworkの型 | 説明 |
---|---|---|
bool | System.Boolean | trueまたはfalseのどちらかとなる論理値。既定値はfalse。 |
byte | System.Byte | 0~255の値を保存する符号なしバイト。既定値は0。 |
sbyte | System.SByte | -128~127の値を保存する符号付きバイト。既定値は0。 |
char | System.Char | 16ビットの符号なしUnicode文字。既定値はnull。 |
decimal | System.Decimal | 丸め計算の対象にならない小数。金融計算によく使用される。既定値は0.0m。 |
double | System.Double | 倍精度浮動小数点数型。既定値は0.0d。 |
float | System.Single | 単精度浮動小数点数型。既定値は0.0f。 |
int | System.Int32 | 32ビットの符号付き整数型。既定値は0。 |
uint | System.UInt32 | 32ビットの符号なし整数型。既定値は0。 |
long | System.Int64 | 64ビットの符号付き整数型。既定値は0。 |
ulong | System.UInt64 | 64ビットの符号なし整数型。既定値は0。 |
object | System.Object | クラスインスタンスへの参照。既定値はnull。 |
short | System.Int16 | 16ビットの符号付き整数型。既定値は0。 |
ushort | System.UInt16 | 16ビットの符号なし整数型。既定値は0。 |
string | System.String | 文字列オブジェクトへの参照。既定値はnull。 |
C#の型と.NET Frameworkの型の意味は同じですが、これらの型を使用する場合はC#の型名を使用してください。そうすることで、コードが簡潔になり、読みやすくなります。ただし、他の言語を使用するコンポーネント開発者と共同作業を行う場合は、表2-1の内容を覚えておくと、2つの言語の型を結び付けるのに役立つでしょう。
2.1.1| .NETの共通型システム
共通型システム(CTS:Common Type System)は、.NET Frameworkの共通言語ランタイム(CLR:Common Language Runtime)での型の使い方を定義する仕様であり、.NET Frameworkの重要な要素です。C#でstringを使用すると、Visual Basic .NETの開発者と同じstringを使用することになります。C#のboolは、Eiffelプログラマが使用するBOOLEAN型と同じです。.NETの開発者は、データと型を簡単に共有できるという恵まれた環境にあります。.NETが登場するまでは、特殊な変換レイヤがないとデータや型を共有できませんでした。
CTSには、C#をはじめとする.NET言語の多くが、CLRによってタイプセーフを保証されるという大きなメリットもあります。CLRはプログラムが安全に実行されることを保証するので、メモリアドレスをうっかり上書きする、スタックを破壊する、メモリのランダム領域に書き込むということはありません。
2.1.2| 共通言語仕様
C#など、.NETプラットフォームの言語が相互にやり取りするための規則は、共通言語仕様(CLS:Common Language Specification)に細かく定義されています。CLSには、複数の言語を使用する開発者がデータ型を再利用できるように、データ型を公開したり整理したりするための方法が規定されています。CLSの規則は、コードが内部的に整理される方法には言及せず、一般に公開されるふるまいに焦点を合わせています。CLSのすべての規則を満たすコンポーネントを構築した場合、そのコンポーネントは「CLS準拠」と見なされます。コードがCLSの規則に1つでも違反すると、コンポーネントはCLS準拠ではなくなり、そのコンポーネントとは別の言語で書かれたプログラムでは利用できない可能性があります。
CLSへの準拠は望ましいことですが、コンポーネントが公開する型とコンポーネントが公開される方法の2つが制限されます。一部のプログラミング言語は、サイズが同じであっても符号付きの型と符合なしの型を正しく処理しない場合があります。このため、byte型以外のC#のほとんどの符合なしスカラデータ型は、CLSに準拠していません。表2-2に、C#のそれぞれの型がCLSに準拠しているかどうかを示します。
▼ 表2-2 C#の型とCLSへの準拠
C#の型 | CLSへの準拠 |
---|---|
bool | ○ |
byte | ○ |
sbyte | × |
char | ○ |
decimal | ○ |
double | ○ |
float | ○ |
int | ○ |
uint | × |
long | ○ |
ulong | × |
object | ○ |
short | ○ |
ushort | × |
string | ○ |
2.2 | クラス
C#は、優れたオブジェクト指向の設計方法を促進する、最新のオブジェクト指向言語です。CやC++を使っていたプログラマは、C#言語のさまざまな要素に見覚えがあるでしょう。しかし、C#にはいくつか根本的な違いがあります。たとえば、C#言語では、クラス宣言の外側に関数を定義できません。また、C++よりもはるかに充実したクラスライブラリを備え、統一された型システムを持ち、ガベージコレクションを使ってプログラマを細かなメモリ管理から解放します。
すべてのオブジェクト指向言語がそうであるように、C#はクラスを通じて再利用を促進します。クラスは型を定義すると同時に、型のインスタンスとやり取りするためのメソッドやプロパティを定義します。たとえば、動物を表すクラスには、動物の行動(食べたり眠ったりなど)を表すメソッドと、その特徴(色や大きさなど)を表すプロパティを定義します。
2.2.1| Visual C# .NETでのクラスの追加
Visual C# .NETで既存のプロジェクトにクラスを追加するには、[ソリューションエクスプローラ]ウィンドウでプロジェクトのアイコンを右クリックし、[追加]から[クラスの追加]を選択します(または、[ソリューションエクスプローラ]ウィンドウでプロジェクトを選択し、[プロジェクト]メニューから[クラスの追加]を選択します)。これにより、図2-1のような[新しい項目の追加]ダイアログボックスが表示されます。
▲ 図2-1 Visual C#プロジェクトにクラスやその他の型を追加するための[新しい項目の追加]ダイアログボックス
[新しい項目の追加]ダイアログボックスは、新しいクラスやその他の型をVisual C# .NETプロジェクトに追加するために使用します。新しいクラスを追加するには、[クラス]アイコンをクリックして、新しいクラスのソースファイルの名前を入力し、[開く]をクリックします。Visual C# .NETによってソースファイルが作成され、編集の画面が開かれます。
2.2.2|クラスの宣言
次に、C#でクラスを宣言するための構文を示します。
public class Cat
{
public Cat(string aName)
{
_name = aName;
}
public void Sleep()
{
_sleeping = true;
}
public void Wake()
{
_sleeping = false;
}
//メンバ変数
protected bool _sleeping;
protected string _name;
}
ここでは、classキーワードを使ってCatクラスを定義しています。クラスを構成するメンバ変数とメソッドの宣言は、クラス宣言の一部として、最も外側の中かっこの内側で定義されています。C++と異なり、C#ではクラスの宣言と実装を同じファイルで行います。これにより、プログラミングモデルが単純になり、クラスの実装が含まれているファイルが常にわかります。一般には、どのソースファイルにも1つのクラスを含めます。複数のクラスを1つのソースファイルに実装することも可能ですが、それは悪いプログラミング作法と見なされます。
C#とC++のクラスには、この他にも次のような違いがあります。
- C#では、すべてのクラスメンバに特定のアクセスレベルを割り当てなければならない。アクセスレベルを宣言しないメンバは、既定でprivateとなる。
- C#には#includeディレクティブがない。C#コンパイラはプロジェクトをグローバルに解析し、依存関係を自動的に解決する。
- クラス宣言の後にセミコロンは必要ない。C#コンパイラは、余分な句読点がなくても、クラスの終端を判断することができる。
この例では、クラスの各メンバの前にpublicまたはprivateというキーワードが付いています。これは、メンバをクラスの外側から参照および使用できるかどうかを規定します。メンバとクラスのアクセスレベルの定義については、「2.2.7 アクセスレベル」で説明します。
この例では、Catがオブジェクトではなくクラスであることに注意してください。クラスは型の定義であり、オブジェクトはその型のインスタンスです。クラスからオブジェクトを生成するには、newキーワードを使用します。次に示すように、newキーワードはオブジェクトのストレージを割り当て、オブジェクトの参照を変数に代入します。
Cat anastasia = new Cat("Anastasia");
このステートメントは2つの部分で構成されています。最初の部分は、Cat型のanastasiaという変数を宣言します。次の部分はCatオブジェクトを生成し、オブジェクトの参照をanastasiaという変数に保存します。Catの後ろにはかっこが必要です。
オブジェクトを生成した後は、次に示すように、クラスが公開するメンバ変数とメソッドを使用できます。
anastasia.Sleep();
メンバへのアクセスには、ドット演算子(.)を使用します。これが、メンバへのアクセスに->演算子または::演算子を使用するC++との違いです。C#には->や::などの演算子はありません。ドット演算子は、型のメンバにアクセスするための唯一の手段です。
オブジェクトはスタック上には生成されず、常にメモリの一部に生成されます。使用されていないリソースは、ガベージコレクション(詳しくは第3章で説明します)と呼ばれるプロセスによって自動的に解放されます。オブジェクトにオブジェクト参照変数が関連付けられていない場合、オブジェクトは既定値としてnullを持ち、有効な参照として使用できなくなります。C#では、値を代入する前に変数を使用することはできないので、他のプログラミング言語に見られる一般的なエラーの多くが解消されています。次のコードのように、null参照を使用しようとすると、CLRがnull参照を検出してNullReferenceException例外を生成します。
Cat c = null;
//許可されない。null参照例外が発生する
c.Eat();
2.2.3| 継承の概要
オブジェクト指向言語を使用すると、現実の事象をモデル化するクラスが作成しやすくなります。動物クラスの例でいうと、何種類かの動物を整理する1つの方法は、動物をクラスに分けて完全に説明することです。5種類の動物を説明するには、5つのクラスを作成します。次の図2-2に示すように、これらのクラスは各動物の特徴を詳しく説明します。
▲ 図2-2 動物をモデル化する単純なクラス
図2-2は、動物をモデル化する5つのクラスを示すUML(Unified Modeling Language)ダイアグラムです。UMLは、クラス間のリレーションシップを説明するための共通モデル化ツールです。この静的なクラスダイアグラムでは、それぞれのボックスがクラスを表し、その中にクラス名とメンバが含まれています。この図が示すように、5つのクラスはいくつかの基本的な動物のごく単純なモデルを提供しています。しかし、どのクラスも他のクラスと同じ関数をいくつか実装しています。オブジェクト指向設計には、型の類似性を利用する手段として、「継承」という概念があります。
継承を利用すれば、クラス間の類似点を洗い出し、それらの類似点を子クラスによって共有される基本クラスに定義できます。たとえば、どの動物クラスもEat(食べる)およびSleep(眠る)という共通の特徴を持っています。歩く動物も泳ぐ動物もいますが、次の図2-3に示すように、共通の特徴は他のクラスが共有できる1つのクラスにまとめることができます。
▲ 図2-3 継承を使用した動物のモデル化
図2-3では、Animalクラスが新しく追加されています。Animalクラスには、すべての動物に共通するメソッドが含まれます。さらに、図2-4に示すように、種類によって動物を分けることができます。
▲ 図2-4 多階層の継承
図2-4では、2つの新しいクラスが追加されています。追加されたWaterAnimalクラスとLandAnimalクラスはAnimalクラスを特殊化したクラスであり、特定の種類の動物に特有なメソッドが含まれます。図2-4では、基本クラスとそのサブクラスとの関係は、サブクラスから基本クラスに引かれた矢印で示されています。最後の5つのオリジナルクラスは、その動物に特有のメソッドだけを実装する、さらに特殊化されたクラスです。
このように、基本クラスを定義することで、新しい動物をより簡単にクラス化できます。たとえば、Penguinクラスを追加する場合は、WaterAnimalから新しいクラスを派生させ、RentTuxedoメソッドを追加するだけです。
抽象クラスの宣言
動物クラスのようなクラス階層を宣言する際、基本クラスが不完全なために、そのままではインスタンスを生成できないことがあります。このような場合は、AnimalクラスやWaterAnimalクラスから直接オブジェクトを生成する代わりに、WhaleクラスやGoldfishクラスを作成します。
C#では、インスタンスを直接生成しない基本クラスには、abstractキーワードを付けます。
abstract class Animal
{
...
}
基本クラスにabstractキーワードを使うと、コンパイラは基本クラスを直接インスタンス化できないようにすることで、クラス階層が正しく使用されるようにします。
抽象基本クラスにメソッドのシグネチャを定義し、そのクラスではメソッドを実装しない場合は、次に示すようにabstractキーワードを付けます。
abstract class Horse
{
abstract public void ChaseAfterBadGuys();
}
抽象メソッドについては、「2.2.6 継承とメソッド」の「抽象メソッド」を参照してください。
シールクラスの宣言
C#は、抽象クラスの逆のクラス、つまり基本クラスとしては利用できないクラスもサポートしています。次のように、クラスをsealedキーワードを使って宣言することで、基本クラスとして使用してはならないことをコンパイラに指示します。
sealed class Horse
{
}
クラスは通常、シールされません。これは、拡張性がなくなるためです。しかし、ただ1つの特定のクラスを一定の実装で使用し、サブクラスを許可しない場合には、クラスを明示的にシールします。これは、信頼性の問題や商用目的において考えられます。また、パフォーマンス上の理由でクラスをシールすることもあります。シールクラスがロードされると、ランタイムはシールクラスへのメソッドの呼び出しを最適化できます。サブクラスのための仮想メソッド呼び出しがないことが明らかなためです。
C#では、個々のメソッドもシールとして宣言できます。これにより、メソッドがサブクラスでオーバーライドされるのを防げます。
基本クラスとのやり取り
C++と同様に、C#ではthisキーワードを通じて現在のオブジェクトにアクセスできます。また、C#ではbaseキーワードを通じて直接の基本クラスのメンバにアクセスできます。次のコードは、基本クラスのPreDrawメソッドを呼び出します。
public void DrawShape()
{
base.PreDraw();
Draw();
}
baseキーワードを使ってアクセスできるのは、直接の基本クラスに限られます。動物の例でいうと(図2-4を参照)、baseキーワードを使ってAnimalのメソッドをWhaleクラスから呼び出すことはできません。
class Whale: WaterAnimal
{
...
// うまくいかない。基本クラスのメソッドはWaterAnimalからしか呼び出せない
base.Eat();
...
}
baseキーワードはクラスの内側でのみ使用できます。次のように、クラスの外側から呼び出すことはできません。
// うまくいかない。このコンテキストで基本クラスを利用することはできない
BessieTheCow.base.GetPlansForWorldDomination();
継承と所有
継承は、クラスを使って現実の事象をモデル化するすばらしい手段ですが、適切な場合のみ使用するように注意してください。次の例に示すように、継承はIS-Aリレーションシップと呼ばれるものをモデル化するのに役立ちます。
- A Cow IS-A LandAnimal(牛は陸の動物である)
- A Whale IS-A WaterAnimal(くじらは海の動物である)
- An Airplane IS-A Vehicle(飛行機は乗り物である)
どのケースも、継承を使用できる見込みが高いです。ただし、次に示すような、所有(HAS-Aリレーションシップ)をモデル化するための継承には注意が必要です。
- A BaseballTeam HAS-A HomeStadium(野球チームはホームスタジアムを持っている)
- A House HAS-A Foundation(家屋には土台がある)
HAS-Aリレーションシップは、継承ではなくメンバ変数が必要であることを示します。こうしたケースは継承には適さず、無理に継承しようとすると実装が難しくなります。
2.2.4| object基本クラス
C#のすべての型は、元をたどればobjectクラスから派生しています。もちろん、すべてのクラスがobjectクラスから直接派生するわけではありませんが、継承階層をたどると、最終的にはobjectクラスにたどり着きます。実際、クラスに継承を指定しないと、objectクラスを継承するためのコードがコンパイラによって自動的に生成されるほど、objectクラスからの派生はC#のプログラミングに欠かせない要素となっています。つまり、次の2つのクラス定義はまったく同じ意味になります。
class Cat
{
...
}
class Cat: object
{
...
}
C#のobject型は、.NET FrameworkのSystem.Objectクラスとまったく同じです。ただし、C#のプログラミング作法では、.NET Frameworkの名前ではなくC#の名前を使うのがよいとされています。 |
C#プログラマが利用できるビルトイン型も、objectクラスから派生しています。すべての型が共通の基本クラスを持っているので、どの型でも同じ処理を行うことができます。これが、ビルトイン型がオブジェクト指向言語のせっかくの利点を逃しているC++との違いです。たとえば、C#ではToStringメソッドを呼び出すことで、すべての型を文字列に変換できます。
int n = 32;
string stringValue = n.ToString()
あるいは、int型の宣言を省略し、ToSrtingを直接代入することもできます。
string stringValue = 32.ToString()
objectクラスを通じて利用できるメソッドには、この他にGetTypeがあります。このメソッドは、オブジェクトの型を実行時に判断できるTypeオブジェクトを返します。この他にも、ガベージコレクション、等価であるかの評価、オブジェクトの複製などを行うメソッドがあります。
2.2.5| メンバ
ほとんどのC#クラスには、1つ以上のメンバが定義されます。C#クラスのメンバは、何らかの機能を提供し、インスタンスの状態を管理します。クラスのメンバは、次のカテゴリに分類されます。
- フィールド── データを保存するための場所。フィールドについては、この後の「フィールド」で詳しく説明する。
- 定数── 値の変わらないクラスのメンバ。定数については、この後の「定数」で詳しく説明する。
- メソッド── オブジェクトまたはクラスのタスクを実行する。メソッドについては、この後の「メソッド」で詳しく説明する。
- コンストラクタ── 初めて生成されるときにオブジェクトを初期化する。コンストラクタについては、この後の「コンストラクタ」で詳しく説明する。
- デストラクタ── オブジェクトを破棄するときに後処理を行う。デストラクタについては、この後の「デストラクタ」で詳しく説明する。
- プロパティ── クライアントからはインスタンス変数として認識され、クラスの実装の詳細からクライアントを引き離し、パブリックメンバに簡単にアクセスできるようにする。プロパティについては、第4章で詳しく説明する。
- 演算子── 一般的な式の演算子とオブジェクトとの相互作用を定義する。演算子については、第4章で詳しく説明する。
- イベント── コールバックの実装を複雑にせずに、クライアントに通知を行えるようにする。イベントについては、第6章で詳しく説明する。
- インデクサ── クラスのインスタンスがクライアントから配列として認識されるようにする。インデクサについては、第7章で詳しく説明する。
以降では、ほぼすべてのC#プログラムで使用するクラスのメンバについて説明します。演算子やインデクサのような特殊なメンバについては、他の章で取り上げます。
フィールド
フィールドはC++のメンバ変数に近いものです。フィールドは、型、名前、必要に応じてアクセスレベルが指定され、クラスに宣言されます。
public int _age;
次に示すように、フィールドの宣言では初期値を定義できます。これは、型の既定値が適切ではない場合に有用です。
public int _answer = 42;
既定では、どのオブジェクトもすべてのフィールドの専用のコピーを持ちます。1つのオブジェクトでフィールドを変更しても、フィールドにstatic修飾子を付けると、そのフィールドはクラスのすべてのインスタンスによって共有されます。
static public int _objectCount;
静的フィールドは、複数のオブジェクトに共有され、複数のオブジェクトからアクセスされますが、同期は自動的にはとられません。複数のスレッドがフィールドを同時に変更しようとした場合、その結果は予測できません。
静的フィールドは、オブジェクトではなくクラスに特化するものなので、オブジェクト参照を通じてアクセスすることはできません。代わりに、クラス名を使って参照します。
class Cat: Animal
{
public static int mouseEncounters;
}
static void Main()
{
Cat c = new Cat();
// 許可されない。静的メンバにはクラス名を使用しなければならない
c.mouseEncounters = 0;
// OK
Cat.mouseEncounters = 0;
}
フィールドはreadonly修飾子で宣言することもできます。
public readonly int _age;
public static int _maxClasses;
フィールドをreadonlyとして宣言すると、オブジェクトが生成された後にフィールドが変更されるのを防ぐことができます。この場合、コンパイラは代入をエラーと見なします。readonlyフィールドは代入を使って宣言できます。さらに、static修飾子を使うこともできます。readonlyフィールドは、最初に宣言されるとき、またはそのクラスのコンストラクタの内側でのみ、値を代入できます(コンストラクタについては、この後の「コンストラクタ」を参照)。
定数
定数はフィールドに似ていますが、値はコンパイル時に指定された値から変化しません。定数はオブジェクト内のスペースを消費せず、必要に応じて実行可能ファイルにコンパイルされます。定数を宣言するには、次のようにconst修飾子を使います。
public const int _maxSize = 9;
定数メンバをstaticとして宣言しようとすると、C#コンパイラによってエラーと見なされます。constメンバの値は、最初に宣言されるときに割り当てられます。constとして宣言されたメンバは特定のオブジェクトに関連付けられないため、静的メンバのようにふるまいます。ただし、constメンバをstaticとして定義する必要はありません。定数メンバはconst修飾子だけで定義し、クラス名を使ってアクセスします。
ClearRange(MyClass._maxSize);
メソッド
C#のクラスのメソッドには、クラスとして実行されるコードが含まれます。C#ではスタンドアロン関数やグローバル関数がサポートされないため、C#のすべてのメソッドはクラスメンバとして存在します。メソッドは常に、クラス宣言の一部として宣言されます。C++とは違い、クラス宣言とは別にメソッドの実装を宣言することはできません。C#のメソッド宣言は、コンストラクタとデストラクタ(この後の「コンストラクタ」および「デストラクタ」を参照)を除いて、アクセスレベル、戻り値の型、メソッド名、メソッドに渡される任意の数の仮パラメータで構成されます。
public int GetNextRecordId(string TableName)
{
...
}
アクセスレベルを省略すると、既定のアクセスレベルはprivateとなります。
フィールドと同様に、メソッドはstaticとして宣言できます。また、これもフィールドと同様に、静的メソッドはオブジェクト参照ではなくクラス名を使って呼び出さなければなりません。
class Cat: Animal
{
public static void ChaseMouse()
{
...
}
}
Class CatApp
{
static void Main()
{
Cat c = new Cat();
// 許可されない。静的メンバにはクラス名を使用しなければならない
c.ChaseMouse();
// OK
Cat.ChaseMouse();
}
...
}
メソッドのパラメータ
C#に既定のパラメータはありません(既定のパラメータは、メソッド宣言の一部として既定値を定義し、クライアントがパラメータを指定せずにメソッドを呼び出した場合に、既定値が自動的に挿入されるようにします)。この機能は、コンポーネントのバージョン化を考慮して許可されていません。既定のパラメータは呼び出し側のコードで挿入されるため、既定のパラメータを更新すると、古いバージョンのクラスをコンパイルしているクラスでバージョン問題が発生します。既定のパラメータの利点の多くは、メソッドのオーバーロードや他の設計パターンという形で手に入れることができます。
既定では、メソッドのパラメータは値渡しとなります。つまり、メソッドはパラメータの値に実際にアクセスするのではなく、パラメータの値のコピーだけにアクセスします。
static void Main()
{
int testValue = 12;
Console.WriteLine("The test value is {0}", testValue);
TrainInVain(testValue);
Console.WriteLine("The test value is {0}", testValue);
}
public static void TrainInVain(int param)
{
param = 42;
}
この例では、WriteLineメソッドの呼び出しはどちらもtestValueの値として12を表示します。TrainInVainメソッドはparamの値を変更しますが、これはMainメソッドから渡される実際のパラメータのコピーにすぎません。TrainInVainの制御が呼び出し側に戻っても、元のパラメータは変化しません。オブジェクト参照が値渡しされると、参照の値がメソッドに渡されるため、メソッドはオブジェクトに完全にアクセスできるようになります。ただし、メソッドがオブジェクト参照自体の値を変更することはできません。
パラメータ修飾子
refキーワードは、パラメータを参照渡しするときに使用します。次に示すように、パラメータの値は呼び出し側のメソッドから更新できるようになります。
static void Main()
{
int testValue = 12;
Console.WriteLine("The test value is {0}", testValue);
ChangeValue(ref testValue);
Console.WriteLine("The test value is {0}", testValue);
}
public static void ChangeValue(ref int param)
{
param = 42;
}
この場合、1つ目のWriteLineメソッドはtestValueの値として12を表示しますが、2番目のWriteLineメソッドは42を表示します。refキーワードはパラメータの種類を変更します。また、呼び出し側だけでなく、呼び出されたメソッドのパラメータリストにも使用しなければなりません。呼び出し側がrefキーワードを省略すると、コンパイラはそれをエラーと見なします。
outキーワードはrefキーワードに似ていますが、outとして定義されたパラメータに値を渡すことはできません。out修飾子が付いたパラメータは、メソッドが呼び出し側に制御を戻すときに値を返します。
static void Main()
{
int testValue;
GetValue(out testValue);
Console.WriteLine("The test value is {0}", testValue);
}
public static void GetValue(out int param)
{
param = 42;
}
この場合のWriteLineメソッドの出力では、testValue値として42を表示します。refキーワードと異なり、out修飾子は呼び出されるメソッドに負担をかけます。C#コンパイラは、out修飾子を使って呼び出されるメソッドがパラメータに値を代入するものと考えます。これは、変数を使う前に値を代入しなければならない規則と同じで、品質保証の手段です。refキーワードと修飾されたパラメータには、呼び出し側が値を代入しなければなりません。
パラメータの最後の修飾子は、パラメータの変数をメソッドに渡すためのものです。paramsキーワードには1次元配列を関連付けなければならず、パラメータリストの最後のエントリとして使用しなければなりません。また、どのメソッド宣言でも、params修飾子は1つだけ使用できます。
static void Main()
{
PrintValues(42);
PrintValues(1, 2, 3, 5, 7, 11, 13);
}
public static void PrintValues(params int[] aListToPrint)
{
foreach(int val in aListToPrint)
{
Console.WriteLine(val);
}
}
この例では、PrintValuesメソッドが2種類の長さのパラメータリストを使って呼び出されます。メソッドは配列を最後まで順番に読み取り、配列の各要素の値を出力します。配列については、第3章で詳しく説明します。foreachステートメントは、C/C++のforステートメントを単純にしたものです。こちらについては第4章で説明します。
メソッドのオーバーロード
C#ではメソッドのオーバーロードが可能であり、メソッドの複数のバージョンをクラスに定義できます。オーバーロードとは、名前が同じでパラメータリストが異なるメソッドを宣言することです。
public void Eat(CatFood food)
{
...
}
public void Eat(Bird theBird)
{
...
}
public void Eat(Bird theBird, int howMany)
{
...
}
オーバーロードメソッドが呼び出されたら、コンパイラはそのメソッド呼び出しに最も一致するものを決定しなければなりません。一致するものが見つからない場合、コンパイラはメソッド呼び出しをエラーと見なします。
Duck aDuck = GetDuck();
// エラー:Eatのオーバーロードはduckを受け付けない
theCat.Eat(aDuck);
オーバーロードメソッドの呼び出しを正確に解決するプロセスは、次の2つの手順で行われます。
- メソッドを調べ、実際のパラメータリストを使ってメソッドを呼び出すことができるかどうかを確認する。
- 各パラメータと一致する度合いで、メソッドをランク付けする。
最も条件を満たしているメンバ関数が1つに絞られた場合、そのメソッドが呼び出されます。適切なメソッドが見つからない場合、コンパイラはメソッド呼び出しを無効であるという理由で拒否します。最も条件を満たしているメンバ関数が複数ある場合、コンパイラはあいまいであるという理由でメソッド呼び出しを拒否します。
コンストラクタ
コンストラクタは、オブジェクトを初期化するために呼び出される特殊なメソッドです。コンストラクタには常にそれを包含しているクラスと同じ名前を付けます。戻り値はありません。コンストラクタには、次の2種類があります。
- インスタンスコンストラクタ ── 特定のオブジェクトのインスタンスを初期化するために使用される。
- 静的コンストラクタ ── クラスコンストラクタとも呼ばれ、クラスを最初に使用する前に呼び出される。
インスタンスコンストラクタは、オブジェクト生成の一環として実行されます。newキーワードを使ってオブジェクトを生成する場合、コンストラクタは次のように呼び出されます。
class Orangutan: LandAnimal
{
public Orangutan()
{
_bugCount = 100;
}
...
}
static void Main()
{
Orangutan tan;
//コンストラクタを呼び出す
tan = new Orangutan();
}
コンストラクタは、他のメソッドと同じようにオーバーロードできます。オーバーロードのプロセスは、通常のメソッドと同じです。それぞれのオーバーロードにはそれぞれ異なるパラメータリストを定義しなければなりません。オブジェクトを生成される際に渡すパラメータを基に、呼び出しの対象となるオーバーロードコンストラクタが決定されます。
class Orangutan: LandAnimal
{
public Orangutan()
{
_bugCount = 100;
}
public Orangutan(int StartingBugCount)
{
_bugCount = StartingBugCount;
}
public Orangutan(string Name)
{
_name = Name;
}
...
}
static void Main()
{
//Orangutan()を呼び出す
Orangutan tan1 = new Orangutan();
//Orangutan(int)を呼び出す
Orangutan tan2 = new Orangutan(100);
//Orangutan(string)を呼び出す
Orangutan tan3 = new Orangutan("Stan");
}
基本クラスの初期化
オブジェクトを生成する作業の一部として、サブクラスのコンストラクタは基本クラスのコンストラクタを呼び出します。このプロセスは、C#コンパイラが挿入する呼び出しにより、暗黙的に行われます。
class LandAnimal
{
...
}
class Orangutan: LandAnimal
{
public Orangutan()
{
_bugCount =100;
}
...
}
このときC#コンパイラは、Orangutan コンストラクタのコードを実行する前にLandAnimal基本クラスの呼び出しを挿入します。この呼び出しは、コンストラクタの初期化子を使って明示的に行うこともできます。
public Orangutan(): base()
{
_bugCount = 100;
}
オーバーロードコンストラクタは、自身のパラメータリストに一致する基本クラスのコンストラクタを自動的に検索するわけではありません。代わりに、基本クラスの既定のコンストラクタを暗黙的に呼び出しますが、それが正しい手順であるとは限りません。通常、コンストラクタの初期化子は、次のコードが示すように、基本クラスの既定以外のコンストラクタを呼び出すために使用されます。これは、サブクラスと基本クラスがいくつかのオーバーロードコンストラクタを共有する場合、特にコンストラクタの初期化子がコンストラクタに渡されるすべてのパラメータにアクセスする場合に役立ちます。
class Orangutan: LandAnimal
{
// Nameパラメータを使って基本クラスを初期化する
public Orangutan(string Name): base(Name)
{
_bugCount = 100;
}
...
}
オブジェクトはまだ生成されていないので、コンストラクタはthisポインタにアクセスできません。thisまたはメンバにアクセスしようとする行為は、コンパイラによって拒否されます。
class Orangutan: LandAnimal
{
// うまくいかない:_bugCount は参照できない
public Orangutan(string Name): base(_bugCount)
{
_bugCount = 100;
}
protected long _bugCount;
...
}
メンバの初期化
コンストラクタのコードのほとんどは、メンバ変数を初期化するために使用されます。これは、C#、C++、Visual Basicをはじめ、多くの言語に共通する特徴です。C#には、「メンバ初期化子」があります。メンバ初期化子は、コンストラクタの本文を実行する前に、インスタンスフィールドを初期化するための手段です。インスタンスメンバは宣言された順番に初期化されます。メンバ初期化子を作成するには、次のようにメンバ宣言に代入式を追加します。
class AnimalReport
{
public string _name = "unknown";
public static int _counter;
...
}
初期化子の式で、他のメンバフィールドやメソッドを参照することはできません。すべてのメンバの初期化を1か所にまとめることができるので、メンバ初期化子は必要となるコードの量を減らし、エラーが発生する可能性を少なくします。静的メンバは、インスタンスメンバと同じ方法で初期化できますが、必ずインスタンスメンバよりも先に初期化されます。初期化の順番は、次のとおりです。
- 静的フィールドが初期化される。
- 静的コンストラクタが呼び出される。
- インスタンスフィールドが初期化される。
- インスタンスコンストラクタが呼び出される。
静的コンストラクタ
静的コンストラクタはインスタンスコンストラクタに似ていますが、クラスのインスタンスが生成される前に自動的に呼び出され、通常はクラスの静的メンバとのやり取りに使用されます。静的メンバは暗黙的にpublicと宣言され、アクセス指定子を持ちません。次に示すように、静的コンストラクタは、static修飾子が追加された既定のインスタンスコンストラクタのように定義されます。
class Player
{
static Player()
{
_counter = -1;
}
public Player(string Name)
{
...
}
...
}
静的コンストラクタが呼び出されるタイミングは決まっていません。また、関連のないクラスで静的コンストラクタの順番を指定する手段は特にありません。実際の順番がどのようになるとしても、次のイベントが発生することに注意してください。
- 静的コンストラクタはクラスの最初のインスタンスが生成される前に呼び出される。
- 静的コンストラクタは一度だけ呼び出される。
- 静的コンストラクタは静的メンバの初期化子の後に実行される。
- 静的コンストラクタは静的メンバが参照される前に呼び出される。
デストラクタ
デストラクタは、クラスのインスタンスを破棄するのに必要なコードを含むメンバです。C#では、C++と同じ構文を使ってデストラクタを宣言します。デストラクタは、先頭にチルダ(~)が付いたクラスを使って宣言します。
~Orangutan()
{
StopGrooming();
}
C++と異なり、C#のデストラクタは必ず呼び出されるとは限りません。C#のデストラクタは、C++のように参照がスコープを外れたときに呼び出されるわけではありません。C#のデストラクタは、ガベージコレクションの過程でオブジェクトが収集されるときに呼び出されます。ガベージコレクションは自動的に処理を行うので、呼び出す必要があることはほとんどありません。ガベージコレクションについては第3章で詳しく説明しますが、C#のデストラクタに関しては、最後の参照が削除されたしばらく後に呼び出されたり、まったく呼び出されなかったりする可能性があることに注意してください。不足しているリソースを解放しなければならないクラスは、別の方法を使ってリソースを解放しなければなりません。一般的な設計パターンとしては、リソースを明示的に解放するCloseメソッドまたはDisposeメソッドを実装します。これらについても第3章で説明します。
2.2.6| 継承とメソッド
継承は、メソッドの宣言方法や呼び出し方法を変更します。基本クラスからサブクラスを派生させるという手段は、基本クラスに存在するメソッドをサブクラスで再定義するのに効果的です。また、サブクラスは基本クラスのメソッドの実装を隠ぺいするのにも役立ちます。
抽象メソッド
抽象メソッドは、基本クラスがメソッドの有効な実装を提供できない場合に役立ちます。C++とは対照的に、C#の抽象メソッドはメソッド本体を持ちません。サブクラスが抽象メソッドを実装しない場合、サブクラスを直接インスタンス化することはできません。
抽象メソッドを実装する場合は、次のコードに示すように、クライアントがoverrideキーワードを使ってその意思をはっきりと伝えなければなりません。これにより、基本クラスとサブクラスをいくつかのバージョンに分ける場合、または別々に開発する場合に発生する可能がある、いくつかのエラーを防ぐことができます。サブクラスと基本クラスの意図を明示的に示すことで、エラーが発生する可能性を減らすことができます。
class Palomino: Horse
{
override public void ChaseAfterBadGuys()
{
...
}
}
仮想メソッド
前述したように、abstractキーワードを使用すると、基本クラスが派生クラスにメソッドの実装を要求できるようになります。C#にvirtualキーワードを使って宣言されるメソッドは、同じような役目を果たします。つまり、サブクラスによる新しい実装の提供を(要求するのではなく)許可します。次に、仮想メソッドの宣言の一例を示します。
class LandAnimal
{
public virtual void Eat()
{
Chew();
Swallow();
}
}
基本クラスに仮想メソッドが宣言されている場合、サブクラスはoverrideキーワードを使って、基本クラスのバージョンをオーバーライドします。overrideキーワードは、抽象メソッドと同様に、次のように仮想メソッドに使用されます。
class Cat: LandAnimal
{
public override void Eat()
{
PlayWithFood();
Chew();
Swallow();
}
}
仮想メソッドの呼び出しでは、最上位の派生メソッドが呼び出されます。通常はこれで構いませんが、常にそうとは限りません。よく知られている「壊れやすい基本クラス(FBC:Fragile Base Class)問題」という問題のために、基本クラスのメソッドを隠ぺいしたい場合もあります。次に、この問題を具体的に説明します。
- LandAnimalという基本クラスが作成され、配置される。
- Orangutanというサブクラスが作成され、LandAnimalクラスの他のサブクラスと一緒に配置される。
- このフレームワークがよくできていたので、新しいバージョンが開発される。この新しいバージョンでは、LandAnimalを担当する開発者がGroomという新しい仮想メソッドを追加する。
- 残念ながら、Orangutanクラスには既にGroomというメソッドが存在し、Groomがoverrideキーワードで宣言されていないために、コンパイルができなくなる。
この例では、基本クラスを変更したために、サブクラスにメンテナンス上の問題が発生しています。Groomメソッドのオリジナルバージョンは、次のように定義されています。
class Orangutan: LandAnimal
{
public void Groom(Orangutan other)
{
other.CleanFur();
other.LookForBugs();
other.EatBugs();
}
}
C#では、OrangutanクラスのGroomメソッドをnewキーワードで宣言するという方法で、この問題を解決できます。C#のnewキーワードは、仮想基本クラスのメソッドを隠ぺいするために使用されます。メソッドをnewキーワードで宣言すると、そのメソッドが仮想メソッドの検索の対象に含まれないことがコンパイラに通知されるので、仮想メソッドを基本クラスに追加しても、既存のコードの動作に支障はありません。
class Orangutan: LandAnimal
{
new public void Groom(Orangutan other)
{
CleanFur();
LookForBugs();
EatBugs();
}
}
基本クラスの参照を通じてGroomメソッドを呼び出すと、newキーワードで修飾されていないGroomメソッドの最上位の派生バージョンが呼び出されます。Orangutanクラスの参照を通じてGroomメソッドを呼び出すと、Orangutan.Groomメソッドが呼び出されます。
2.2.7| アクセスレベル
C#プログラムの型とメンバには、特定のアクセスレベルが割り当てます。表2-3に示すように、クラスのメンバには5段階のアクセスレベルが割り当てられます。
▼ 表2-3 C#クラスのメンバのアクセスレベル
アクセスレベル | 説明 |
---|---|
public | 自由にアクセスできる。 |
protected | 包含しているクラスとサブクラスにアクセスが許可される。 |
internal | 包含しているアセンブリのクラスにアクセスが許可される(アセンブリについては後述)。 |
protected internal | 包含しているアセンブリのクラスまたは包含しているクラスのサブクラスにアクセスが許可される。 |
private | 包含しているクラスだけにアクセスが許可される。 |
次に示すように、各メンバにはアクセスレベルを割り当てなければなりません。割り当てなかった場合、コンパイラは既定のアクセスレベルを使用します。
public class Employee
{
// Nameはパブリックのアクセスレベルを持つ
public string Name;
// EmployeeId はプライベートのアクセスレベルを持つ
string EmployeeId;
...
}
型のアクセスレベル
どの型にも、何段階かのアクセスレベルを割り当てることができます。その型に何も宣言しない場合、既定のアクセスレベルが定義されます。型が宣言されている場所によって、既定のアクセスレベルが異なる場合があります。最上位に定義されている型(トップレベルの型)には、既定でパブリックのアクセスレベルが割り当てられます。クラスの内部に定義されている型には、既定でプライベートのアクセスレベルが割り当てられます。
// トップレベルクラスの既定のアクセスレベルはパブリック
class MilkFactory
{
public MilkFactory()
{
Console.WriteLine("Milk factory");
}
}
class Cow{
// 埋め込みクラスの既定のアクセスレベルはプライベート
class MilkFactory
{
public MilkFactory()
{
Console.WriteLine("Cow milk factory");
}
}
}
クラスの内側に定義される型は実装の詳細なので、最適なアクセスレベルはプライベートと考えられます。つまり、すべてのクラス宣言にアクセスレベルを明示的に指定しない限り、クラス定義を移動するだけでアクセスレベルは変化します。
Visual C# .NETを使ってプロジェクトを作成すると、トップレベルクラスはそれが既定のアクセスレベルであるにもかかわらず、自動的にpublicキーワードで修飾されます。 |
トップレベルの型のもう1つのオプションは、internalとして定義することです。これにより、そのクラスを包含しているアセンブリのコードだけにアクセスが許可されます。
// アセンブリの内部でのみアクセスできる
internal class Cow: ITempermental
{
...
}
他のアクセスレベルは適用されないので、トップレベルの型をpublicまたはintenalのどちらかで定義するとよいでしょう。型が他の型にネストされている場合、アクセスレベルオプションは変わることがあります。
インターフェイスのメンバと列挙型
インターフェイス(「2.4 インターフェイス」で後述)のメンバは、常にパブリックです。インターフェイスの目的はパブリックメソッドを宣言することなので、インターフェイスメソッドの可視性を下げるためのオプションはありません。同様に、列挙型(「2.8 列挙」で後述)は、スカラ値を指定するためのメカニズムを提供します。列挙型のメソッドは常にパブリックであり、アクセスレベルを下げることはできません。ただし、enum変数には、表2-3に示した5段階のアクセスレベルを割り当てることができます。
構造体のメンバ
構造体のメンバの既定のアクセスレベルはprivateです。これは、構造体のメンバが既定でpublicとなるC++の習慣に反しています。構造体のメンバには、public、private、またはinternalのアクセスレベルを割り当てることができます。structの継承は不可能なので、構造体のメンバにprotectedのアクセスレベルを割り当てることはできません。
struct Sailboat
{
// すべてのクライアントから完全に参照できる
public int Length;
// アセンブリの内側でのみ参照できる
internal int Beam;
// 構造体の内側でのみ参照できる
private string _secretName;
}
クラスのメンバ
クラスのメンバの既定のアクセスレベルはprivateです。クラスのメンバには、表2-3に示した5段階のアクセスレベルを割り当てることができます。オブジェクト指向プログラミングの世界では、クラスの実装の詳細をクライアントから見えなくすることが奨励されています。このためには、public以外のアクセスレベルオプションを指定します。
ネストされたアクセスレベル
クラスまたは構造体のメンバに割り当てられるアクセスレベルオプションは、包含している型のアクセスレベルにも依存するので、確実ではありません。たとえば、クラスを参照するフィールドがprotectedとして宣言されている場合、クラスのメンバのアクセスレベルは、クラスにパブリックメンバが定義されていたとしても、常にprotected以下となります。
class Horse
{
public void ChaseAfterBadGuys()
{
...
}
}
class Cowboy
{
protected Horse _myHorse;
public Cowboy()
{
_myHorse =new Horse();
}
}
public class CowboyApp
{
static void Main(string[] args)
{
Cowboy cowboyBill = new Cowboy();
// コンパイルされない:_myHorseにはアクセスできない
LoneRanger._myHorse.ChaseAfterBadGuys();
}
}
この例では、メソッドにpublicのアクセスレベルが割り当てられていても、Cowboyクラスを通じてHorseクラスのメソッドにはアクセスすることはできません。_myHorseメンバ変数にはprotectedのアクセスレベルが割り当てられているため、Horseのメンバがそれよりもオープンなアクセスレベルを持つことはできません。
既知のfriendへのアクセス許可
C#言語には、C++で誤用されがちなfriendキーワードがありません。表2-3に示した5段階の保護はC++の保護レベルから発展したものであり、クラスのプロテクトメンバやプライベートメンバをinternalとして宣言することで、それらをアセンブリの内側から参照できるようにします。これは、コンポーネントの開発者がコンポーネントのユーザーにアクセスを許可しない場合でも、コンポーネントの内側でメンバを内部的に公開できる、C#の便利な機能です。
2.3 | 名前空間
第1章で概説したように、メインアプリケーションのクラスも含め、すべての型は名前空間に定義されます。名前空間を明示的に指定しなかった場合、既定のグローバル名前空間に置かれます。C#で使用される名前空間は、C++など、他の言語で使用される名前空間に似ています。.NET Frameworkレベルでは、型の名前には名前空間が含まれます。例として、次のクラスを見てみましょう。
namespace RailRoad
{
public class Train
{
...
}
}
C#の型は、この例のように名前空間を使って宣言しますが、.NET Frameworkでは、宣言された型の名前はRailRoad.Trainとして認識されます。C#では、この型をRailroad.Trainとして参照できます。後者の名前も有効ですが、扱いにくいのであまり使用されません。名前空間の概念を持たない他の.NET言語は、完全名を使って型を参照します。
2.3.1|新しい名前空間の作成
第1章で示したように、新しい名前空間は、次のようにnamespaceキーワードを使って作成します。
namespace MSPress.CSharpCoreRef.Animals
{
...
}
名前空間のメンバは、複数のファイルに定義できます。つまり、名前空間全体を1つのソースファイルに定義する必要はありません。コンパイラは、複数のソースファイルで同じ名前空間を使用していることを検出すると、それを1つにまとめます。
プログラミング作法の問題ですが、名前空間のルートには会社の名前または他の一意の識別子を使用し、階層を表すようにしてください。会社名をルートノードとして名前空間を作成すれば、他の開発者が作成する名前空間との不幸な衝突を避けることができます。本書のソースコードの多くは、MSPress.CSharpCoreRef名前空間を使用しています。
C++の名前空間と異なり、C#の名前空間は全体を1行で定義できます。
<会社名>.<コンポーネント領域>.<クラスクラスタ>
すべてのクラスを1つの名前空間に定義する必要はありません。大規模なプロジェクトでは、名前空間の階層を作成するとよいでしょう。たとえば、アプリケーションに再利用できるコンポーネントを開発する場合は、名前空間を図2-5のように整理できます。
▲ 図2-5 大規模なプロジェクトにおける典型的な名前空間
2.3.2| 名前空間の使用
ウィザードを使って作成されたVisual C# .NETのクラスには、既定でSystem名前空間への参照が含まれます。.NET Frameworkの他の名前空間に定義されているデータ型を使用するには、2つの方法があります。1つ目の方法では、名前空間を含む型の完全名を使用します。
System.Xml.XmlTextReader tr;
もう1つの方法では、usingキーワードを使って、現在の名前空間に名前空間を追加します。
using System.Xml;
これで、型のインスタンスを宣言する際に、型の名前だけを指定できるようになります。
XmlTextReader tr;
2つの型が別の名前空間に同じ名前で定義されている場合は、コンパイラのためにあいまいさを解決しなければなりません。例として、別の名前空間に定義された2つのIdというクラスを使用するプログラムを見てみましょう。
namespace BankTypes
{
public class Id
{
string _identifier;
public Id(string Identifier)
{
_identifier = Identifier;
}
string GetId()
{
return _identifier;
}
}
}
namespace ClubTypes
{
public class Id
{
string _identifier;
public Id(string Identifier)
{
_identifier = Identifier;
}
string GetId()
{
return _identifier;
}
}
}
ClubTypesとBankTypes の2つの名前空間に、それぞれIdというクラスが定義されています。ソースファイルでは2つの名前空間を自由に使用できますが、Idオブジェクトを生成しようとすると、あいまいさが原因でコンパイルエラーが発生します。コンパイラがエラーを返すのは、Idという名前が2つのクラス(ClubTypes.IdおよびBankTypes.Id)を参照する可能性があるためです。
IntelliSenseを有効にすると、ソースファイルの編集中にこの問題が警告されるようになります。この例のように名前空間と型が宣言されている場合、次の行をコードに挿入すると、IntelliSenseがIdという型名があいまいであること、さらに修飾が必要であることが警告されます。
Id myId = new Id("CSharpCoreRef");
どちらかのId型を使用する場合は、それを完全に修飾しなければなりません。
ClubTypes.Id myClubId = new ClubTypes.Id("CSharpCoreRef");
2.3.3| 参照の追加
.NET Frameworkでは、利用可能な型が「アセンブリ」としてパッケージ化されます。アセンブリは、.NETのアプリケーションおよびコンポーネントのバージョンと配置の単位です。最も基本的な形式では、型を含んだ1つのEXEモジュールまたはDLLモジュールとなります。
型を使用するには、その型が含まれているアセンブリへの参照を追加しなければなりません。コマンドラインからプログラムをビルドする場合は、次に示すように、/referenceスイッチ(または省略形の/rスイッチ)を使って、適切なアセンブリへの参照を追加します。
csc /reference:system.xml.dll; animals.cs
Visual Studio .NETでアセンブリへの参照を追加するには、[ソリューションエクスプローラ]ウィンドウで[参照設定]アイコンを右クリックし、[参照の追加]を選択します。図2-6のような[参照の追加]ダイアログボックスが表示されます。
▲ 図2-6 [参照の追加]ダイアログボックス
[.NET]タブには、グローバルアセンブリキャッシュで使用できるすべてのアセンブリが表示されます。プロジェクトから参照するアセンブリを選択するには、[選択]をクリックします。グローバルアセンブリキャッシュにないアセンブリを選択するには、[参照]をクリックしてプライベートアセンブリを探します。
外部アセンブリへの参照は、アセンブリの「マニフェスト」と呼ばれる部分に追加されます。マニフェストには、ダイナミックリンクライブラリ(DLL)の外部依存の一覧が含まれています。.NET Frameworkは、マニフェストの依存リストを使って、コードに使用する型をロードします。特定の.NET Framework型のアセンブリについては、MSDNライブラリで確認できます。各型の説明には、その名前空間とアセンブリの情報が含まれています。
2.4 | インターフェイス
C#のインターフェイスは、C++の純粋な抽象クラスに似ています。インターフェイスはクラスに似ていますが、メンバ変数を持たず、インターフェイスメソッドの実装も持ちません。インターフェイスは、単に派生クラスが実行できる作業を説明するものにすぎません。
2.4.1| インターフェイスの使用
インターフェイスをインスタンス化することはできません。
// うまくいかない。直接インスタンス化できない
ISpeak speaker = new ISpeak();
次に示すように、インターフェイスから派生するクラスを作成し、クラスのインスタンスを生成する必要があります。
class Cat: ISpeak
{
...
}
Cat anastasia = new Cat();
「ISpeakオブジェクト」というのは、正しい表現ではありません。正確には、「ISpeakインターフェイスを実装するオブジェクト」となります。
インターフェイスを実装するクラスは、インターフェイスに含まれているすべてのメソッドを実装しなければなりません。インターフェイスのメソッドが1つでも実装されないと、クラスはインスタンス化できません。
interface ISpeak
{
void StringToSpeech(string Message);
void TextFileToSpeech(string FileName);
}
class SpeechEngine: ISpeak
{
public void StringToSpeech(string Message)
{
...
}
//TextFileToSpeechが実装されていない。
}
この例では、SpeechEngineクラスがISpeakインターフェイスから継承したTextFileToSpeechメソッドを実装していません。インターフェイスから継承する非抽象クラスはすべてのメンバを実装しなければならないので、コンパイラはこれをエラーと見なします。SpeechEngineクラスがabstractとして宣言されていれば、実装されていないメンバメソッドをabstractとして宣言できるようになるので、エラーにはなりません。クラスや実装されていないメソッドをabstractとして宣言すれば、実装を先送りするという意思が明らかになります。
2.4.2| クラスとインターフェイス
ここまでは、C#でクラスを作成する方法と継承のしくみについて説明しました。先ほど説明したように、C#言語は基本クラスの多重継承をサポートしていませんが、複数のインターフェイスの実装をサポートしています。これにより、クラスが多重継承と同じ利益を享受できる場合があります。
.NET Frameworkでは、インターフェイスを実装するのか、それとも基本クラスから直接派生させるのかで悩むことがあります。たとえば、新しいコレクションクラスを作成する場合は、IList、ICollection、IEnumerableインターフェイスを実装するCollectionBaseクラスからの派生と、それらのクラスの直接実装という2つの選択肢があります。この3つのインターフェイスを実装するクラスは、.NET Frameworkが提供する基本コレクションクラスと同じように使用できます。
通常は、.NET Frameworkが提供する基本クラスの機能を利用するのがよいでしょう。しかし、基本クラスの機能を利用できないほど新しいクラスが特殊である場合は、インターフェイスを使ってクラスを実装する価値があるかもしれません。
2.5 | 例外によるエラー処理
どのようなソフトウェアプログラムでも、ある程度の規模になるとエラーが生じるものです。コーディングエラー、実行時エラー、システムのエラーなど、エラーの形式はさまざまです。エラーの受け取り方や検出される場所にかかわらず、すべてのエラーが何らかの方法で処理される必要があります。C#では、すべてのエラーは例外イベントと見なされ、例外処理メカニズムによって処理されます。
例外処理は、エラーを処理するための強力な手段を提供し、エラーの発生に応じて制御や豊富なエラー情報を渡します。例外が発生すると、エラー状態からの回復を行う適切な例外ハンドラに制御が渡されます。
エラー処理に例外を使用するコードは、他のエラー処理方法を使用するコードに比べ、すっきりしていて信頼性が高い傾向にあります。一例として、戻り値を使ってエラーを処理する次のコードを見てみましょう。
bool RemoveFromAccount(string AccountId, decimal Amount)
{
bool Exists = VerifyAccountId(AccountId);
if(Exists)
{
bool CanWithdraw = CheckAvailability(AccountId, Amount);
if(CanWithdraw)
{
return Withdraw(AccountId, Amount);
}
}
return false;
}
戻り値を使ってエラーを検出するコードには、次の問題点があります。
- ブール値が返されたら、別のエラーパラメータを使用するか、他のエラー処理関数を呼び出して、エラーの詳細な情報を入手しなければならない。Win32 APIのほとんどは、この方法を使用する。
- エラーコードや正常終了コードを返すには、それぞれのプログラマがエラーコードを正しく処理しなければならない。エラーコードは無視されたり誤解されたりすることが多く、結果としてエラー状態が見逃されたり、誤解されたりする可能性がある。
- エラーコードの拡張や更新は困難である。新しいエラーコードを追加するために、ソースコード全体を更新しなければならないことも考えられる。
C#では、エラー情報は例外と一緒に渡されます。簡単なエラーコードやブール値を返す代わりに、失敗したメソッドが例外をスローすることが求められます。例外は、エラー状況に関する情報を豊富に含んだオブジェクトです。
2.5.1| 例外処理
例外を処理するには、tryブロックを使ってコードブロックを保護し、例外を処理するためにcatchブロックを1つ以上定義します。
try
{
Dog d;
// null参照を通じて呼び出しを試みる
d.ChaseCars();
...
}
catch
{
// 任意の例外を処理する
...
}
tryブロックで例外がスローされると、正しい型の例外オブジェクトが生成され、適切なcatch句の検索が開始されます。この例では、catch句が例外の種類を指定していないので、あらゆる例外が捕捉されます。プログラムの実行は直ちにcatch句に制御を移し、次のどちらかの処理を行います。
- 例外を処理し、catch句の後から実行を続ける。
- 現在の例外を再スローする。
特定の例外だけを捕捉するために、catch句はフィルタの役目を果たすパラメータを提供しています。次に示すように、処理の対象となる1つ以上の例外を指定します。
try
{
Dog d;
// null参照を通じて呼び出しを試みる
d.ChaseCars();
...
}
catch(NullReferenceException ex)
{
// null参照の例外のみを処理する
...
}
例外を引き起こしたエラーをこのcatch句で適切に処理できた場合は、catch句の後から実行が続けられます。catch句でエラーを処理できない場合は、他の例外ハンドラでエラーからの回復を試みることができるように、catch句で例外を再スローする必要があります。
例外を再スローするには、catch句に渡された例外オブジェクトにthrowキーワードを使用します。
try
{
Dog d;
// null参照を通じて呼び出しを試みる
d.ChaseCars();
}
catch(NullReferenceException ex)
{
// エラー情報をログに記録する
LogError(ex);
// 例外を再スローする
throw(ex);
}
通常のcatchブロックでは、throwキーワードは単体で使用します。
catch
{
...
// 現在の例外を再スローする
throw;
}
例外が再スローされると、例外を処理できる次のcatch句の検索が行われます。現在のメソッドの呼び出しに関与したメソッドシーケンスを割り出すために、呼び出し履歴が検査されます。呼び出し履歴の各メソッドは、適切なcatch句が定義されている場合に、例外を処理できる可能性があります(ただし、直前の呼び出しメソッドに最初に渡される例外は含まれません)。このメソッドシーケンスは、例外が処理されるか、呼び出し履歴のすべてのメソッドに例外を処理する機会が与えられるまで続きます。例外用のハンドラが見つからない場合、例外が処理されないことが原因でスレッドは終了します。
例外情報の使用
次に示すように、例外オブジェクトのToStringメソッドを呼び出すと、.NET Frameworkの例外から適切な説明を取り出すことができます。
catch(InvalidCastException badCast)
{
Console.WriteLine(badCast.ToString());
}
例外オブジェクトには、次のような情報も用意されています。
- StackTrace── 例外のスロー時に捕捉されたスタックトレースを文字列で返す。
- InnerException── 例外の処理中に保存された追加の例外を返す。
- Message── 例外を説明するローカライズされた文字列を返す。
- HelpLink── 例外に関する情報をさらに提供するURLまたはURN(Universal Resource Name)を返す。
外部に公開する独自の例外型を作成する場合は、少なくともこれらの情報を提供しなければなりません。.NET Frameworkの例外と同じ情報を提供すれば、例外はより使いやすいものになるでしょう。
複数の例外ハンドラの提供
数種類の例外がスローされる可能性がある場合は、複数のcatch句を追加して、例外オブジェクトに一致するものが見つかるまでcatch句を順番に評価できます。次のように、catch句の1つは例外の種類を指定しない汎用句にします。
try
{
Dog d = (Dog)anAnimal;
}
catch(NullReferenceException badRef)
{
// null参照を処理する
...
}
catch(InvalidCastException badCast)
{
// 不適切なキャストを処理する
...
}
catch
{
// 残りの例外を処理する
...
}
catch句は順番に評価されるため、汎用的なcatch句は具体的なcatch句の後ろに配置してください。よく考えずにcatch句を配置すると、思わぬ結果につながります。
try
{
Dog d =(Dog)anAnimal;
}
catch(Exception ex)
{
// 任意の例外を処理する
}
catch(InvalidCastException badCast)
{
// 不適切なキャストを処理する
}
この例では、最初のcatch句がすべての例外を処理してしまい、2番目のcatch句に例外が渡されることはありません。このため、無効なキャストの例外(InvalidCastException)が期待どおりに処理されません。
また、包含する側のtry...catchブロックにtry...catchブロックを配置して、例外ハンドラをネストすることも可能です。
try
{
Animal anAnimal = GetNextAnimal();
anAnimal.Eat();
try
{
Dog d = (Dog)anAnimal;
d.ChaseCars();
}
catch(InvalidCastException badCast)
{
// 不適切なキャストを処理する
...
}
}
catch
{
// 汎用の例外処理
...
}
例外がスローされると、例外はまず最も近いcatch句に渡されます。このcatch句で例外が処理されなかったり、例外が再スローされたりした場合は、包含する側のtry...catchブロックに渡されます。
コード実行の保証
メソッドを作成するとき、メソッドが呼び出し側に制御を戻す前に、必ず実行しなければならない処理が存在することがあります。たとえば、リソースを解放するなど、例外や他のエラーが発生した場合でも実行しなければならない処理です。C#では、このようなコードをfinally句に配置できます。finally句は、メソッドが制御を戻す前に実行されることが保証されています。
try
{
Horse silver = GetHorseFromBarn();
silver.Ride();
}
catch
{
...
}
finally
{
silver.Groom();
}
この例では、例外がスローされた後にfinally句が実行されます。finally句は、return、goto、continue、またはbreakステートメントが、try句から制御を移動しようとしたときにも実行されます。
このように、finally句とcatch句は同じtry句に定義できます。catch句が例外を再スローすると、制御はメソッドから離れる前に、まずfinally句に渡されます。
2.5.2| .NET Frameworkの例外
.NET Frameworkには、多数の例外クラスが定義されています。すべての例外クラスは元をたどるとSystem名前空間のExceptionクラスから派生していますが、.NET Frameworkで使用される設計パターンでは、例外を生成する名前空間に例外が定義されます。たとえば、IOクラスがスローする例外は、System.IO名前空間に定義されます。
.NET Frameworkの例外が、System.Exceptionクラスを直接継承することはほとんどありません。.NET Frameworkの例外は、クライアントが同じような例外を1つのグループにまとめるための共通のサブクラスを使って、カテゴリ別に分類されます。たとえば、すべてのIO例外は、System.Exceptionから派生したSystem.IO.IOExceptionクラスのサブクラスです。このため、クライアントに単一のcatchブロックでIO関連のエラーを処理する例外ハンドラを記述できます。例外ごとに専用のハンドラを作成する必要はありません。
catch(IOException ioError)
{
// すべてのIO例外をここで処理する
}
独自の例外クラスを開発する際には、System.Exceptionクラスから直接派生させる代わりに、さまざまなサブクラスから派生させることを検討してください。これにより、例外がクライアントのコードから使いやすくなるだけでなく、新しいコードを記述せずに、クラスからスローされる例外を処理できる可能性もあります。
2.6 | キャスト
C#の「キャスト」は、値や参照を別の型に明示的に変換するために使用されます。キャストを行うにはキャスト演算子を使用します。次に示すように、型をかっこで囲みます。
Type T2 newType = (T2)oldType;
サブクラスは必ず基本クラスに置き換えることができるので、サブクラスを基本クラスの参照に代入する場合、キャストは要求されません。しかし、逆の場合は必要です。
// OK:基本クラスの参照への代入
Animal a = aDog;
// エラー:キャストが必要
Dog d = anAnimal;
// OK:キャストを使った変換
Dog d2 = (Dog)anAnimal;
一般に、クライアントにサブクラスの機能が必要となる場合、基本クラスにも同じ機能があるとは想定できないため、明示的なキャストが要求されます。基本クラスへの参照が、実際には派生クラスへの参照であることがわかっている場合は、キャストによる変換を行うでしょう。しかし、想定していないサブクラスが検出されたなどの理由で、キャストが実行できない場合はどうなるでしょうか。キャストの実行は本質的に危険であり、変換を実行しようとしても、そうした変換が許可されていなければ失敗します。次の、Dogオブジェクトを期待しているメソッドにCatオブジェクトが渡されるという作為的な例では、変換が失敗するとInvalidCastException例外がスローされます。
class Animal
{
public void Eat()
{
Console.WriteLine("Eating");
}
}
class Cat: Animal
{
public void ChaseBirds()
{
Console.WriteLine("Chasing birds");
}
}
class Dog: Animal
{
public void ChaseCars()
{
Console.WriteLine("Chasing cars");
}
}
class TestAnimal
{
static void ChaseACar(Animal anAnimal)
{
Dog d = (Dog)anAnimal;
d.ChaseCars();
}
static void Main()
{
Cat c = net Cat();
ChaseACar(c);
}
}
無効なキャストの例外を処理するには、完全に安全とはいえないキャストに対して、必要な例外処理を定義しなければなりません。
static void ChaseACar(Animal anAnimal)
{
try
{
Dog d = (Dog)anAnimal;
d.ChaseCars();
}
catch(InvalidCastException badCast)
{
//エラー回復コード
}
}
一般に、クラスの無効なキャストは、コンパイル時にエラーとして捕捉されません。こうしたエラーを正確に検出できるのは実行時に限られます。しかし、例外をスローすると大げさになりすぎる場合もあります。失敗したら別の処理をするという方法が適していることもよくあります。C#では、次のようにasキーワードを使用すると、変換を試みても例外はスローされません。
Dog d = anAnimal as Dog;
asはキャストと同様に、変換元の型から変換先の型への変換を試みますが、キャストと異なり、変換が失敗しても例外をスローしません。代わりに、変換が失敗したことを検出できるように、参照にnullを設定します。
static void ChaseACar(Animal anAnimal)
{
Dog d = anAnimal as Dog;
if(d != null)
d.ChaseCars();
else
Console.WriteLine("Not a dog");
}
2.7 | 構造体
CやC++と同様に、C#でも構造体型を作成できます。構造体は、複数の型のメンバを組み合わせて1つの新しい型を作成する集合型です。クラスと同様に、構造体のメンバは既定でプライベートであり、クライアントにアクセスを許可するには明示的にパブリックで宣言しなければなりません。次のように、構造体はstructキーワードを使って宣言します。
struct Point
{
public int x;
public int y;
}
2.7.1| 構造体と継承
構造体は継承できません。
struct Point
{
public int x;
public int y;
}
// 許可されない。structは継承できない
class ThreeDPoint: Point
{
public int z;
}
これは、クラスと構造体に実質的な違いがほとんどないC++とは対照的です。C#の構造体はインターフェイスを継承できますが、クラスや他の構造体は継承できません。
2.7.2| 構造体の割り当て
クラスのインスタンスと異なり、構造体にはヒープが割り当てられず、スタック上に割り当てられます。ヒープに割り当てられないことで、構造体のインスタンスの実行効率はクラスのインスタンスを凌ぐ場合もあります。たとえば、使用と破棄が1つのメソッド呼び出しの中で行われる一時的な構造体を複数作成することは、オブジェクトをヒープから作成するよりも効率的です。これに対し、構造体をメソッド呼び出しのパラメータとして渡すには、構造体のコピーを作成しなければならないので、オブジェクトの参照を渡すよりも効率はよくありません。
構造体のインスタンスの生成は、クラスの新しいインスタンスの生成に似ています。
Point pt = new Point();
構造体のメンバにアクセスするには、必ずドット演算子を使用します。
pt.y = 5;
2.7.3| メンバ関数
構造体には、メンバフィールドの他にメンバ関数があります。また、コンストラクタを定義することもできますが、コンストラクタにはパラメータを1つ以上定義しなければなりません。
struct Rect
{
public Rect(Point topLeft, Point bottomRight)
{
top = topLeft.y;
left = topLeft.x;
bottom = bottomRight.y;
right = bottomRight.x;
}
// 高さと幅が負の座標を持たない長方形と仮定する
public int Area()
{
return (bottom - top)*(right - left);
}
// 頂点を返す
int top, bottom, left, right;
}
構造体にデストラクタを宣言することはできません。
2.8 | 列挙
「列挙」は、特定の型に関連付けられる値を並べたリストです。列挙を宣言するには、enumキーワードを使用します。
enum Color
{
Red, Yellow, Green
}
列挙の値にアクセスするには、ドット演算子を使用します。
Color background = Color.Red;
C++の列挙子と異なり、C#の列挙子はenumという型名を付けて使用しなければなりません。これにより、列挙間で名前が競合する可能性が少なくなり、読みやすさも向上します。
列挙は、列挙リストの各列挙子にスカラ値を割り当てます。列挙値を保存するための既定の基本型はintであり、次のように任意のスカラ値に変更できます。
enum Color : byte
{
Red, Yellow, Green
}
列挙子の値は、初期値(この場合は1)が指定されなければ、0から始まります。
enum Color : byte
{
Red = 1, Yellow, Green
}
列挙子に初期化子が指定されない限り、列挙子には連続する値が割り当てられます。列挙子の値は一意でなければなりませんが、連続している必要や順番に並んでいる必要はありません。
enum Color : byte
{
Red = 1, Yellow = 42, Green = 27
}
列挙はint型(または別のスカラ値)をベースとしますが、次のように明示的に指定しないと、列挙をintに代入したりintを列挙に代入したりすることはできません。
Color background = (Color)27;
int oldColor = (int)background;
まとめ
Visual C# .NETでは、CTSをベースとする充実したビルトイン型はもちろん、.NET Frameworkのクラスライブラリにもアクセスできます。すべてのオブジェクト指向言語がそうであるように、C#ではビルトイン型とクラスライブラリを使って、アプリケーション用の新しい型が構築できます。他のユーザーが利用できるように新しい型を公開するには、他のプログラミング言語を使用している開発者ができるだけ利用しやすいように、CLSに準拠した型を作成します。
C#は、C++をはじめとするオブジェクト指向言語の特徴の多くを共有しています。C#は、仮想メソッド、抽象クラス、メソッドのオーバーライドをサポートしています。また、派生クラスでメソッドをオーバーライドするための優れたモデルや、高性能な例外処理モデルなど、再利用可能で高性能なクラスの作成を支援するための機能を提供します。
第3章では、値型と参照について説明します。オブジェクトの有効期間、ストレージ、ガベージコレクションなど、値型と参照の重要な違いについて検討します。また、第3章ではプロパティについても取り上げ、どのようにすればクラスがより使いやすくなるかについて説明します。