クラスの宣言と定義


コンパイラディレクティブ

Objective-C 言語はオブジェクト指向型言語なので、再利用可能なデータと制御をパッケージ化したテンプレートを提供することができます。 これを、一般にクラスと呼び、クラスはメモリ実体を生成するための情報です。

クラスの概念は、C 言語の構造体を発展させたもので、構造体のようなデータの関連付けに加えて、データと制御(関数)を関連付けています。 そのため、コードは常にデータを処理する専用の関数を安全に呼び出すことができ、一連の機能を一つのサブシステムとして提供することができるのです。

クラスを利用するには、構造体と同様にまず宣言が必要です。 Objective-C 言語のクラスの宣言は、予約語や構文ではなくコンパイラディレクティブを利用します。 コンパイラディレクティブは、コンパイラに Objective-C で拡張されたクラスの宣言や実装などに用いられ、@ マークで始まるという特徴を持ちます。

クラスを宣言するコンパイラディレクティブは @interface で始まり @end で終わります。 これらの間に、クラスに関連付けられる変数領域と関数を宣言します。 Objective-C のクラス宣言と定義の関係は他の言語に比べて複雑なので、最初は戸惑うかもしれません。

@interface クラス名 : 親クラス名
{
	インスタンス変数宣言
	...
}
メソッド宣言
@end

この時点で、C 言語系のオブジェクト指向言語とは宣言方法がかなり違うことが伺えます。 インスタンス変数とは、クラスに関連付けられた変数のことで、構造体のメンバに等しい存在です。 ただし、構造体とは異なりインスタンス変数にクラスの外部からアクセスすることはできません。 インスタンス変数を利用できるのは、原則としてクラスに関連するメソッドだけとされています。 インスタンス変数が存在しない場合はインスタンス変数宣言と { } を省略することができます。

メソッドとは、クラスに関連付けられた関数のことです。 通常の関数との違いは、メソッドはメソッドを呼び出したクラスの実体に関連付けられているため、直接インスタンス変数にアクセスすることができます。

クラス名の直後に親クラスを指定するとありますが、親クラスとは何でしょうか。 オブジェクト指向は、一度定義された機能を拡張する継承と呼ばれる機能を提供しなければなりません。 継承関係は、よく生物の進化論に例えられ、犬や猫などの、様々な実体に共通する抽象的な機能を哺乳類クラスとして定義し、犬クラスや猫クラスは哺乳類クラスを継承するようにプログラムするのです。

親クラスを指定しないクラスは、他のクラスの頂点となるためルートクラスと呼ばれます。 Java や .NET では、プラットフォームが基本となるルーとクラスを提供する仕組みを採用し、C++ 言語はルートクラスを定めずにルートクラスを開発者が開発できるように設計されています。 Objective-C はその中間的な形で、原則ではルートクラスから開発することができますが、事実上はコンパイラなどの処理系がルートクラスを提供しなければなりません。

ルートクラスは処理系によって異なりますが GCC であれば Object クラスが、Mac OS X の Cocoa 環境であれば NSObject クラスがルートクラスとなります。 独自のルートクラスを開発する場合であれば親クラスを指定しませんが、通常は処理系が提供するルートクラスを継承します。 ルートクラスを開発する場合のみ、宣言は次のようになるでしょう。

@interface クラス名
{
	インスタンス変数宣言
	...
}
メソッド宣言
@end

なぜ、ルートクラスを継承しなければならないのかというと、ルートクラスはクラスが正常に動作するために必要な基本的な機能を提供するという役割があるためです。 ルートクラスが存在しなければ、クラスの実体を作成するための煩雑な手続きをすべてのクラスで記述しなければならなくなってしまいます。 ヒープメモリの確保や解放などの基本的な処理手続きは、ルートクラスに任せてしまうことで、開発者は開発目的の機能を記述することに集中できるのです。

インスタンス変数の宣言は通常の変数と同様ですが、メソッド宣言は関数の宣言とはかなり異なります。 この、メソッド宣言の仕様は C++ に比べて Objective-C が C 言語プログラマに受け入れられなかった大きな要因の一つであると考えられるでしょう。 メソッド宣言は、次のようになります。

- (戻り値型) メソッド名 : 仮引数リスト ... ;

なぜ、関数の宣言とこれほども異なるのか、C 言語とのハイブリット言語としての仕様上の問題が浮き彫りになっています。 最初のマイナス記号 - はこのメソッドがクラスの実体であるオブジェクトに関連することを表しています。 通常のメソッドは - 記号で良いのですが、オブジェクトではなくクラスに関連するメソッドの宣言であれば + 記号を指定します。 - 記号のメソッドをインスタンスメソッドと呼び、+ 記号で始まるメソッドをクラスメソッドと呼びます。 インスタンスメソッドとクラスメソッドの違いは後ほど詳しく説明しますが、一般的なメソッドは - から始まると覚えてください。

戻り値型を見てもわかるように、メソッド宣言では型を ( ) で囲みます。 メソッドが引数を受け取る場合は、コロン : に続いて仮引数リストを指定します。 仮引数リストも、通常の変数宣言とは異なり、変数型を ( ) の中に指定し、それに続いて変数名を指定します。

C 言語の関数のデフォルトの戻り値は int 型でしたが、メソッドの戻り値のデフォルトは id 型という Objective-C で追加されたオブジェクトを表す汎用型になっています。 戻り値型は省略することができますが、通常は省略するべきではなく、明示的に宣言するべきでしょう。 戻り値を返さない場合は (void) を指定します。

@interface Test : Object
- (void)method;
@end

この Test クラスの宣言は、戻り値を返さず、引数を受け取らない method メソッドを宣言する単純なクラスを宣言しています。 クラス名やメソッド名の命名規則は、C 言語の変数や構造体名の命名規則と同じで、一意に識別できるアルファベットから始まる識別子でなければなりません。 ただし、習慣としてクラス名は大文字から始まり、メソッド名は小文字から始まります。

さて、クラスとメソッドを無事に宣言することができましたが、メソッドはメソッドの名前と型だけを宣言しただけで、処理コードを記述した定義が存在しません。 実は、Objective-C ではクラスの宣言と定義が完全に分離されていて、クラスの宣言ではインスタンス変数とメソッドの宣言しか行うことができません。 クラスを宣言から定義へと具体化するには @implementation コンパイラディレクティブを用いてクラスを定義しなければなりません。

@implementation クラス名
メソッド定義
...
@end

@implementation コンパイラディレクティブでは、@interface で宣言したクラスの実装を記述します。 宣言したメソッドは、この場で定義されなければなりません。 クラスでメソッドが宣言されていない場合、@implementation に記述するものは何もありませんが、それでも、定義されていないクラスは @implementation で明示的に実装しなければなりません。


クラスのインスタンス化

クラスを宣言、及び定義すれば、クラスを利用することができるようになりますが、そのままでは使うことができません。 クラスを利用するために必要なメモリ領域を割り当て、適切な初期化を行ってはじめてクラスが正常に動作するようになります。 クラスの宣言情報に基づいて、メモリを割り当てることをインスタンス化と呼び、確保されたメモリ領域をインスタンスと呼びます。 インスタンスと呼ぶ場合、通常はクラスの情報に基づいて確保された実体を表しますが、一般的にはオブジェクトとも呼ばれます。

C++ 言語や Java 言語など、一般的なオブジェクト指向型言語であれば new 演算子が存在し、この演算子がクラスの宣言情報に基づいてインスタンスを生成し、初期化してくれます。 しかし、驚くべきことに Objective-C 言語は、インスタンスの生成も含めてクラスが行わなければならないと定められています。 ところが、クラスのインスタンス化には、オブジェクトに必要なサイズを調べ、メモリを割り当てるなど、煩雑な初期化処理が必要になります。 これをすべてのクラスで実装するのは現実的ではないので、この作業を一手に引き受けてくれるのがルートクラスの存在なのです。

Object クラスは、Object クラスを含めたそれを継承するクラスのインスタンスを適切に生成する alloc クラスメソッドを定義してます。 通常のインスタンスメソッドは、メソッドを呼び出すためにインスタンスが必要ですが、クラスメソッドはインスタンスがなくても実行することができるという性質があります。 そのため、alloc メソッドはインスタンスが存在しなくても呼び出すことに問題はありません。 alloc メソッドは、インスタンスを生成するためのクラスメソッドなので、しばしばファクトリメソッドとも呼ばれています。

+ alloc;

これが、Object クラスで宣言されている alloc メソッドです。 + で始まっているため、このメソッドはクラスメソッドであることをアピールしています。 戻り値型は省略されているので、デフォルトの id 型が返されます。

id 型 は、オブジェクトを表す汎用型で、一種の void * のようなものだと考えてください。 つまり、オブジェクトのクラス型がなんであれ、あらゆるオブジェクトは id 型の変数に保存することができます。

さて、クラスを生成するために alloc メソッドを呼び出さなければなりませんが、メソッドはどのように呼び出すのでしょうか。 C++ 系のオブジェクト指向言語を経験した多くのプログラマは、直観的に次のようなコードを想像することでしょう。

id obj = クラス名.alloc();

残念ながら、Objective-C では、多くのオブジェクト指向型言語で採用されてるこの記述方法は誤りです。 Objective-C でクラスメソッドを呼び出すには、次のように記述しなければなりません。

[クラス名 メソッド名:引数リスト...]

これは、Smalltalk 言語をオブジェクト指向部分の語源とする Objective-C の特徴です。 インスタンスメソッドを呼び出す場合は、クラス名の部分にオブジェクトを指定しなければなりません。 C 言語では [ ] は配列の要素を指定するために使われていましたが、コンパイラは [ ] の前後の調べることでメソッドの呼び出しなのか、配列の要素指定なのかを判断することができます。

この、メソッドを呼び出すための [ ] をメッセージ式と呼びます。 Objective-C では、オブジェクトから(関数のアドレスなどを用いて)メソッドを直接呼び出すのではなく、オブジェクトに対してメソッドに関連するメッセージを送信しているのです。 メッセージ式によってオブジェクトにメッセージが送信されると、オブジェクトは与えられたメッセージに従って適切なメソッドを起動しようとするのです。 このような動的な仕組みは柔軟性を確保しますがC 言語の関数の呼び出しに比べ、オーバヘッドは倍以上かかると考えてください。 もちろん、コンパイラが最適化などを行う場合はこの限りではありませんが、メッセージによる動的なメソッドの呼び出しは、実行時にメソッドを検索するため必ずオーバヘッドがかかります。

#import <stdio.h>
#import <objc/Object.h>

@interface Test : Object
- (void)method;
@end

@implementation Test
- (void)method {
	printf("Kitty on your lap\n");
}
@end

int main() {
	id obj = [Test alloc];
	[obj method];
	
	return 0;
}

さて、これが簡単な Objective-C によるクラスの実現です。 @interface によるクラスの宣言では、ルートクラス Object を継承する Test クラスを宣言しています。 Test クラスでは、戻り値を返さない、引数を受け取らない method メソッドを宣言しています。 そのため @implementation でこのメソッドを実装しなければなりません。

クラスの宣言と定義が終了すれば、その後、そのクラスを利用することができるようになります。 main() メソッドでは、オブジェクトを格納する id 型の変数 obj を宣言し、同時に Test クラスのインスタンスを生成してこれに代入しています。 [Test alloc] では Test クラスのクラスメソッド alloc を呼び出すという意味です。 Test クラスで alloc は定義していませんが、alloc メソッドは Object ルートクラスで定義されているため、正常に動作します。 alloc メソッドでは、Test クラスの宣言情報に基づいて適切にメモリを割り当て、生成したオブジェクトを返してくれます。

Test クラスはインスタンスメソッド method を定義しているため、生成したオブジェクトの method メソッドをメッセージ式から起動できます。 プログラムでは、インスタンス生成直後に [obj method] というメッセージ式を記述して、Test クラスの method メソッドを起動しています。 その結果、画面には Kitty on your lap という文字列が表示されるでしょう。

因みに、オブジェクトを使い捨てにしてしまうのであれば、インスタンスの生成と method メソッドの呼び出しは、次のように記述することも可能です。

[[Test alloc] method]

この場合、内側の [Test alloc] が先に実行されて、生成したインスタンスを参照する id 型のオブジェクトが返されます。 そして、返された id 型のオブジェクトに対して、さらに method メッセージを送信することで、メソッドを呼び出しているのです。 このように、メッセージ式は入れ子にすることができます。 これは、(Test . alloc()) . method() というような関数の呼び出し関係に例えることができるでしょう。

上記のプログラムのように、クラスの宣言と定義の場所は自由ですが、習慣としてクラスの宣言はヘッダファイルに、定義はクラス名と同じ名前の *.m ファイルに記述します。



前のページへ戻る次のページへ