|
|
||||||||||||||
|
| < 前ページ次ページ > |
クラス定義は Objective-C プログラミングの中核をなすものですが、クラス定義だけが Objective-C のオブジェクト定義を作成する仕組みではありません。このセクションでは、メソッドの宣言とクラスとメソッドの関連付けを行う、他の 2 つの方法について説明します。
カテゴリ ― 既存のクラスへのメソッドの追加
プロトコル ― 他のクラスが実装できるインタフェースの宣言
クラスにメソッドを追加するには、インタフェースファイルでカテゴリ名の下にメソッドを宣言し、実装ファイルで同じ名前の下にメソッドを定義します。カテゴリ名は、メソッドが、新しいクラスではなく、どこか他の場所で宣言されたクラスへの追加であることを示します。ただし、カテゴリを使って、クラスにインスタンス変数を追加することはできません。
カテゴリは、サブクラスの代わりに使うことができます。既存のクラスを拡張するサブクラスを定義するのではなく、カテゴリによって、クラスに直接的にメソッドを追加することができます。たとえば、NSArray などの Cocoa クラスにカテゴリを追加できます。サブクラスの場合のように、拡張するクラスのソースコードは必要ありません。
カテゴリが追加するメソッドは、クラス型の一部になります。たとえば、カテゴリを使用して NSArray クラスに追加したメソッドは、コンパイラが NSArray インスタンスのレパートリーに含まれていると想定するメソッドの一部となります。NSArray クラスのサブクラスに追加されたメソッドは、NSArray 型には含まれません(静的な型定義はコンパイラがオブジェクトのクラスを認識できる唯一の方法であるため、これが問題になるのは静的に型定義されたオブジェクトに関してのみです)。
カテゴリメソッドは、クラス自体で定義したメソッドで可能なことはすべて実行できます。実行時には、まったく違いがありません。カテゴリがクラスに追加するメソッドは、他のメソッドと同様に、すべてのクラスのサブクラスによって継承されます。
カテゴリインタフェースの宣言は、クラスインタフェース宣言とよく似ています。ただし、クラス名の後にカテゴリ名を丸括弧で囲んでリストアップし、スーパークラスを記述しない点が異なります。メソッドがクラスのインスタンス変数にアクセスしないかぎり、カテゴリは拡張するクラスのインタフェースファイルをインポートする必要があります。
|
この実装は、通常どおり、自身のインタフェースをインポートします。カテゴリにちなんだ名前をインタフェースファイルに付けるとすると、カテゴリ実装は次のようになります。
|
カテゴリでは、クラスの追加インスタンス変数を宣言できないことに注意してください。カテゴリはメソッドだけを含みます。ただし、クラスの有効範囲内にあるすべてのインスタンス変数は、カテゴリの有効範囲内にもあります。これには、クラスで宣言されているすべてのインスタンス変数のほか、@private として宣言されているインスタンス変数も含まれます。
クラスに追加できるカテゴリの数には制限がありませんが、各カテゴリの名前は異なっている必要があり、それぞれが異なるメソッドのセットを宣言して定義する必要があります。
カテゴリに追加したメソッドによって、クラスの機能を拡張したり、クラスが継承するメソッドをオーバーライドすることができます。また、カテゴリではクラスインタフェースで宣言されているメソッドをオーバーライドすることもできます。ただし、同じクラスの別のカテゴリに宣言されているメソッドを確実にオーバーライドすることはできません。カテゴリはサブクラスの代わりではありません。カテゴリでは、クラスの @interface セクションで明示的に宣言されているメソッドの再定義を避けるのが最良です。また、クラスでは同じメソッドを複数回定義できないことにも注意してください。
カテゴリで継承メソッドをオーバーライドする場合、新しいバージョンでは super へのメッセージによって、通常どおり、継承バージョンを組み込むことができます。ただし、カテゴリメソッドには、同じクラスに定義されている同じ名前のメソッドを組み込む方法がありません。
カテゴリを使えば、他の実装者が定義したクラスを拡張することができます。たとえば、Cocoa フレームワークで定義されているクラスにメソッドを追加することができます。追加したメソッドはサブクラスに継承され、実行時にはクラスのオリジナルのメソッドと区別がつきません。
また、カテゴリを使えば、新しいクラスの実装を別々のソースファイルに分散できます。たとえば、大きなクラスのメソッドをいくつかのカテゴリにグループ化し、各カテゴリを異なるファイルに入れることができます。このように使用することで、カテゴリはさまざまな点で開発プロセスに恩恵をもたらします。
また、「プロトコル ― 他のクラスが実装できるインタフェースの宣言」で説明しているように、カテゴリを使って非形式プロトコルを宣言することもできます。
カテゴリは、ルートクラスを含め、任意のクラスにメソッドを追加できます。NSObject に追加したメソッドは、コードにリンクするすべてのクラスで利用可能になります。メソッドの追加は役立つこともありますが、かなり危険な場合もあります。カテゴリによる変更内容は熟知されていて、影響も限定的であるように思えますが、継承によって影響が広範囲に及びます。未知のクラスに予期しない変更を加えてしまう可能性があります。つまり、行ったことの結果をすべて知ることができないかもしれないのです。さらに、他の開発者も変更があったことを知らなければ、変更による影響も理解できません。
さらに、その他にも、ルートクラスにメソッドを実装する際の注意点が 2 つあります。
通常、クラスオブジェクトはクラスメソッドのみを実行できます。しかし、ルートクラスに定義されているインスタンスメソッドは特例です。それのメソッドは、すべてのオブジェクトが継承するランタイムシステムへのインタフェースを定義します。クラスオブジェクトは完全なオブジェクトで、同じインタフェースを共有する必要があります。
この機能は、NSObject クラスのカテゴリで定義するインスタンスメソッドが、インスタンスだけでなくクラスオブジェクトによっても実行される可能性を考慮しなければならないことを意味します。たとえば、メソッドの本文では、self がインスタンスだけでなく、クラスオブジェクトを指すこともあります。ルートインスタンスメソッドへのクラスアクセスの詳細については、Foundation フレームワークリファレンスの NSObject クラス仕様を参照してください。
クラスおよびカテゴリインタフェースでは、特定のクラスに関連付けられるメソッド、主にクラスが実装するメソッドを宣言します。これに対して、非形式および形式プロトコルは、クラスには関連付けられていないながらも、任意の(おそらく多数の)クラスによって実装される可能性のあるメソッドを宣言します。
プロトコルはメソッド宣言の単なるリストで、クラス定義とは結び付いていません。たとえば、マウスに対するユーザ操作を報告する次のメソッドは、プロトコルにまとめることができます。
|
マウスイベントに応答しなければならないクラスは、このプロトコルを採用して、そのメソッドを実装することができます。
プロトコルはメソッド宣言をクラス階層への依存から解放するため、クラスとカテゴリでは使用できない方法でメソッドを使用できます。プロトコルはどこかに実装されている(またはその可能性のある)メソッドをリストしますが、メソッドを実装するクラスを知る必要がありません。知る必要があるのは、特定のクラスがプロトコルに準拠するかどうか、プロトコルに宣言されているメソッドをクラスが実装しているかどうかということです。したがって、同じクラスを継承するという類似性だけでなく、同じプロトコルに準拠するという類似性に基づいても、オブジェクトのタイプをグループ化することができます。継承階層において互いに関係のない分岐にあるクラスも、同じプロトコルに準拠するため、類似のものとして型定義することができます。
プロトコルはオブジェクト指向設計において重要な役割を果たします。特に、プロジェクトを多数の実装者が分担したり、他のプロジェクトで開発されたオブジェクトを組み込む場合に重要となります。Cocoa ソフトウェアは、Objective-C メッセージによるプロセス間通信をサポートするため、プロトコルを大いに利用しています。
しかし、Objective-C プログラムでは、プロトコルを必ず使用する必要はありません。クラス定義やメッセージ式とは異なり、プロトコルの使用は任意です。Cocoa フレームワークにも、プロトコルを使用するものと、使用しないものがあります。プロトコルを使用するかどうかは、行うべき作業によって決まります。
以降のセクションでは、これらの状況と、プロトコルが果たす役割について説明します。
オブジェクトのクラスが分かっていれば、そのインタフェース宣言(および、継承元のクラスのインタフェース宣言)を参照して、そのオブジェクトが応答する対象となるメッセージを調べることができます。これらの宣言は、受信できるメッセージを提示します。プロトコルは、送信するメッセージを提示する方法も提供します。
通信は双方向で機能し、オブジェクトはメッセージを受信するだけでなく、送信もします。たとえば、あるオブジェクトは特定操作の責任を別のオブジェクトにデリゲートするかもしれませんし、あるいは、別のオブジェクトに情報を要求するだけかもしれません。場合によっては、オブジェクトがその動作を他のオブジェクトに積極的に通知し、他のオブジェクトが必要な措置を取れるようにすることも考えられます。
同じプロジェクトの一部として送信側のクラスとレシーバのクラスを開発する場合(あるいは、他からレシーバとそのインタフェースファイルが提供される場合)、この通信は簡単に調整できます。送信側は単に、レシーバのインタフェースファイルをインポートするだけです。インポートしたファイルには、送信側が送信するメッセージで使用する、メソッドセレクタが宣言されています。
しかし、まだ定義されていないオブジェクト(実装が他の人に任されているオブジェクト)にメッセージを送信するオブジェクトを開発する場合は、レシーバのインタフェースファイルがありません。メッセージで使用する、自分では実装しないメソッドを宣言するために、別の方法が必要になります。プロトコルは、このような目的に使用できます。プロトコルは、クラスが使用するメソッドをコンパイラに通知し、オブジェクトを連携させるために定義する必要があるメソッドを他の実装者に知らせます。
たとえば、helpOut: などのメッセージを送信することで、別のオブジェクトに支援を要請するオブジェクトを開発するとします。これらのメッセージのアウトレットを記録する assistant インスタンス変数を用意し、インスタンス変数を設定する付随メソッドを定義します。このメソッドにより、他のオブジェクトは、オブジェクトのメッセージの潜在的受信者として自身を登録することができます。
|
次に、メッセージを assistant に送信するたびに、メッセージに応えるメソッドがレシーバに実装されていることをチェックします。
|
このコードを記述する時点では、assistant としてどのようなオブジェクトが登録されるかは分からないため、可能なのは helpOut: メソッドのプロトコルを宣言することのみです。メソッドを実装するクラスのインタフェースファイルはインポートできません。
プロトコルを使って、匿名オブジェクト 、つまり未知のクラスのオブジェクトのメソッドを宣言することができます。匿名オブジェクトは、サービスを示したり、限られた数の関数から成るセットを処理することができます。特にその種類のオブジェクトが 1 つだけ必要な場合に使用します(アプリケーションのアーキテクチャを定義する際に基本的な役割を果たすオブジェクトや、使用する前に初期化しなければならないオブジェクトは、匿名オブジェクトには適していません)。
もちろん、当該オブジェクトの開発者にとっては匿名ではありませんが、開発者がオブジェクトを他の誰かに提供するときは匿名です。たとえば、次のような状況があるとします。
|
メソッドによって返されたオブジェクトはクラス識別情報を持たないオブジェクト、少なくとも供給側が積極的に公開するような識別情報を持たないオブジェクトです。しかし、多少なりとも役に立つように、供給側は対応できるメッセージの少なくとも一部を積極的に提示する必要があります。これを行うには、プロトコルで宣言したメソッドのリストとオブジェクトを関連付けます。
各アプリケーションは、独自の構造、クラス、および内部ロジックを持ちます。しかし、アプリケーションと通信するためにその動作や構成要素を知っている必要はありません。部外者として知っている必要があるのは、送信可能なメッセージ(プロトコル)とメッセージの送信先(レシーバ)だけです。
リモートメッセージの潜在的レシーバとしてオブジェクトの 1 つを公開するアプリケーションは、オブジェクトが受信したメッセージに対応するために使用するメソッドを宣言するプロトコルも公開しなければなりません。オブジェクトに関して、他には何も公開する必要がありません。送信側アプリケーションは、オブジェクトのクラスを知っていたり、自身の設計の中でそのクラスを使用する必要はありません。必要なのはプロトコルだけです。
プロトコルにより、匿名オブジェクトが可能になります。プロトコルがなければ、クラスを特定せずに、オブジェクトのインタフェースを宣言する方法はありません。
| 注意:匿名オブジェクトの供給側はそのクラスを公開しませんが、オブジェクト自体は実行時に明らかになります。class メッセージは匿名オブジェクトのクラスを返します。しかし、通常はこの付加的な情報を知る意味はほとんどなく、プロトコルの情報だけで十分です。 |
複数のクラスが一組のメソッドを実装する場合、それらのクラスは多くの場合、共通のメソッドを宣言する抽象クラスの下にグループ化されます。各サブクラスは独自の方法でメソッドを再実装できますが、継承階層および抽象クラスの共通の宣言によってサブクラス間の本質的な類似性が範囲されます。
しかし、共通のメソッドを抽象クラスにグループ化できないこともあります。それにもかかわらず、ほとんどの点で関連のないクラスが、いくつかの類似メソッドを実装する必要があるかもしれません。このような限られた類似性は、階層関係にするには十分な理由にならない可能性があります。たとえば、さまざまな種類のクラスが参照カウントを容易にするメソッドを実装することが考えられます(Foundation Framework ですでに参照カウントが実装されているため、これは単なる例です)。
|
これらのメソッドをプロトコルとしてグループ化し、クラスをすべて同じプロトコルに準拠させることで、それらの類似性を反映できます。
オブジェクトはそれらのクラスではなく、このような類似性(クラスが準拠するプロトコル)に応じて型定義することができます。たとえば、NSMatrix はセルを表すオブジェクトと通信しなければなりません。NSMatrix は、これらの各オブジェクトが NSCell (クラスをベースにした型)の一種であることを要求し、NSCell クラスを継承するすべてのオブジェクトが NSMatrix メッセージに応えるために必要なメソッドを持っているものと想定することができます。もう 1 つの方法として、NSMatrix はセルを表すオブジェクトに、特定のメッセージセットに対応できるメソッドを持つことを要求することができます(プロトコルをベースにした型)。この場合、NSMatrix はセルオブジェクトがメソッドさえ実装していれば、どのようなクラスに属しているかは問題にしません。
プロトコルを宣言する最も簡単な方法は、カテゴリ宣言でメソッドをグループ化することです。
|
非形式プロトコルは、NSObject を継承する任意のクラスとメソッド名を緩やかに関連付けるため、通常は、NSObject クラスのカテゴリとして宣言します。すべてのクラスはルートクラスを継承するため、メソッドの対象は継承階層のどの部分にも限定されません(非形式プロトコルを別のクラスのカテゴリとして宣言し、継承階層の特定の分岐に適用対象を限定することも可能ですが、そうするべき理由はほとんどありません)。
プロトコルの宣言に使用する場合、カテゴリインタフェースには対応する実装がありません。その代わりに、プロトコルを実装するクラスは、自身のインタフェースファイルでもう一度メソッドを宣言し、実装ファイルで他のメソッドと一緒に定義します。
非形式プロトコルはカテゴリ宣言のルールに反して、メソッドのグループをリストアップする一方で、それらを特定のクラスや実装と関連付けません。
カテゴリで宣言したプロトコルは非形式プロトコルであるため、言語によるサポートはほとんど受けません。コンパイル時の型チェックも、オブジェクトがプロトコルに準拠しているかどうかを確認する実行時のチェックもありません。これらのメリットを得るには、形式プロトコルを使用する必要があります。非形式プロトコルが適しているのは、すべてのメソッドの実装が任意である場合(デリゲートなど)です。
Objective-C 言語では、メソッドのリストをプロトコルとして形式的に宣言する方法を提供しています。形式プロトコルは、言語とランタイムシステムによってサポートされます。たとえば、コンパイラはプロトコルに基づいて型をチェックすることができ、オブジェクトはプロトコルに準拠しているかどうかを実行時にイントロスペクションを実行して報告することができます。
形式プロトコルは @protocol ディレクティブを使って宣言します。
|
たとえば、参照カウントプロトコルは次のように宣言できます。
|
クラス名と異なり、プロトコル名にはグローバルな可視性がありません。プロトコル名は自身のネームスペースに属します。
クラスは、プロトコルに宣言されているメソッドの実装に同意する場合、形式プロトコルを採用 すると言います。クラス宣言では、採用したプロトコルの名前を、スーパークラス名の後の不等号括弧に入れて示します。
|
カテゴリもほぼ同じ方法でプロトコルを採用します。
|
プロトコルリストの名前はコンマで区切ります。
プロトコルを採用するクラスまたはカテゴリは、プロトコルを宣言するヘッダファイルをインポートする必要があります。採用したプロトコルで宣言されているメソッドは、クラスまたはカテゴリインタフェースの他の場所では宣言されていません。
クラスではプロトコルを採用するだけで、他のメソッドを宣言しないことも可能です。たとえば、次のクラス宣言では、Formatting および Prettifying プロトコルを採用していますが、インスタンス変数や自身のメソッドは宣言していません。
|
プロトコルを採用するクラスまたはカテゴリには、プロトコルが宣言するメソッドをすべて実装する義務があります。そうしない場合、コンパイラが警告を発します。上記の Formatter クラスでは、自身で宣言したものに加えて、採用した 2 つのプロトコルで宣言されているメソッドをすべて定義します。
プロトコルの採用は、ある意味でスーパークラスの宣言に似ています。どちらもメソッドをクラスに割り当てます。スーパークラス宣言は継承メソッドをクラスに割り当て、プロトコルはプロトコルリストに宣言されているメソッドをクラスに割り当てます。
実行時にクラスがクラスオブジェクトによって表され、メソッドがセレクタコードによって表されるように、形式プロトコルは特別なデータ型、すなわち Protocol クラスのインスタンスによって表されます。プロトコルを処理するソースコード(型指定で使用する場合を除く)は、Protocol オブジェクトを参照する必要があります。
さまざまな意味で、プロトコルはクラス定義と似ています。どちらもメソッドを宣言し、実行時にオブジェクトによって表されます。つまり、クラスはクラスオブジェクトによって、プロトコルは Protocol オブジェクトよって表されます。クラスオブジェクトのように、Protocol オブジェクトはソースコードにある定義と宣言から自動的に作成され、ランタイムシステムによって使用されます。プログラムソースコードでの割り当てと初期化は行われません。
ソースコードでは、@protocol() ディレクティブを使用して Protocol オブジェクトを参照することができます。このディレクティブは、プロトコルを宣言するディレクティブと同じですが、後に丸括弧が付いています。この丸括弧にはプロトコル名を入れます。
|
これは、ソースコードで Protocol オブジェクトを呼び出せる唯一の方法です。クラス名と異なり、プロトコル名はオブジェクトを指定しません(@protocol() 内は除く)。
コンパイラはプロトコル宣言に遭遇するたびに Protocol オブジェクトを作成しますが、それは次の場合だけです。
@protocol() を使って)ソースコードのどこかで参照されている宣言したものの使用されていないプロトコル(後述のように型チェックの場合は除く)は、実行時に Protocol オブジェクトによって表されません。
クラスが、自身(またはスーパークラス)がプロトコルに宣言されているメソッドを実装する場合、形式プロトコルに準拠すると言います。クラスのインスタンスは、そのクラスが準拠するのと同じプロトコルのセットに準拠すると言います。
クラスは採用するプロトコルに宣言されているすべてのメソッドを実装する必要があり、それらのメソッドはサブクラスによって継承されるため、クラスまたはインスタンスがプロトコルに準拠するというのは、そのレパートリーの中にプロトコルに宣言されているすべてのメソッドがあると言うのと同じです。
オブジェクトがプロトコルに準拠しているかどうかをチェックするには、conformsTo: メッセージを送信します。
|
conformsTo: テストは、単独のメソッドを対象とする respondsTo: テストによく似ています。ただし、特定のメソッドが実装されているかどうかではなく、プロトコルが採用されているか(そして結果的に、宣言されているメソッドがすべて実装されているか)どうかをテストします。メソッドのリスト全体をチェックするため、conformsTo: のほうが respondsTo: より効率が高いこともあります。
conformsTo: テストは、isKindOfClass: テストにもよく似ています。ただし、継承階層をベースにした型ではなく、プロトコルをベースにした型をテストします。
オブジェクトの型宣言は、形式プロトコルを含むように拡張することができます。そのため、プロトコルはコンパイラによる別のレベルの型チェックの可能性を提供します。プロトコルは特定の実装に結び付いていないので、より抽象的な型チェックになります。
型宣言では、プロトコル名はタイプ名の後の不等号括弧内に記述します。
|
静的型定義では、コンパイラがクラス階層に基づいて型をテストできるのと同様に、この構文では、コンパイラはプロトコルに準拠しているかどうかに基づいて型をテストすることができます。
たとえば、次の宣言で、Formatter が抽象クラスであるとします。
|
上記の宣言は Formatter を継承するすべてのオブジェクトを 1 つの型にグループ化するため、コンパイラはその型を対象に割り当てをチェックすることができます。
次の宣言も同様です。
|
上記の宣言は、Formatting プロトコルに準拠するすべてのオブジェクトをクラス階層の位置に関係なく、1 つの型にグループ化します。コンパイラは、プロトコルに準拠するオブジェクトだけがこの型に割り当てられることを保証できます。
いずれの場合にも、共通の継承を共有するか、共通のメソッドセットを中心にまとめられる類似のオブジェクトが型によってグループ化されます。
2 つの型を 1 つの宣言で一体化することができます。
|
プロトコルは、クラスオブジェクトの型定義には使用できません。クラスとして静的に型定義できるのはインスタンスだけであるのと同じように、プロトコルとして静的に型定義できるのもインスタンスだけです(ただし、実行時には、クラスとインスタンスはどちらも conformsTo: メッセージに応答します)。
プロトコルは、クラスがプロトコルの採用に使うのと同じ構文を使用して、他のプロトコルを組み込むことができます。
|
不等号括弧内に記述されたすべてのプロトコルが、ProtocolName プロトコルの一部と見なされます。たとえば、次のようにして、Paging プロトコルに Formatting プロトコルを組み込みます。
|
この場合、Paging プロトコルに準拠するオブジェクトは、Formatting にも準拠します。次の型宣言を参照してください。
|
また、次の conformsTo: メッセージも参照してください。
|
これらはどちらも、Paging プロトコルを記述するだけで、Formatting への準拠もテストされます。
クラスでプロトコルを採用するときは、前述したように、プロトコルに宣言されているメソッドを実装する必要があります。さらに、採用したプロトコルに組み込まれているすべてのプロトコルにも準拠しなければなりません。組み込まれているプロトコルにさらに他のプロトコルが組み込まれている場合、クラスはそれらにも準拠する必要があります。クラスが組み込まれたプロトコルに準拠するには、次のどちらかの方法があります。
たとえば、Pager クラスで Paging プロトコルを採用しているとします。Pager が NSObject のサブクラスの場合、次のようになります。
|
組み込まれた Formatting プロトコルで宣言されているメソッドを含め、すべての Paging メソッドを実装する必要があります。これは、Paging に加えて Formatting プロトコルも採用します。
これに対して、Pager が Formatter(Formatting プロトコルを独自に採用しているクラス)のサブクラスの場合、次のようになります。
|
Paging プロトコル自体に宣言されているすべてのメソッドを実装する必要がありますが、Formatting に宣言されているメソッドは実装の必要がありません。Pager が Formatter から Formatting プロトコルへの準拠を継承するからです。
クラスは、プロトコルを形式的に採用しなくても、単にプロトコルに宣言されているメソッドを実装するだけでプロトコルに準拠できることに注意してください。
複雑なアプリケーションに取り組んでいるときに、次のようなコードを記述している場合があります。
|
ここで、プロトコル B が次のように宣言されているとします。
|
このような状況では、循環が生じ、どちらのファイルも正しくコンパイルされません。このような再帰的循環を中断するには、プロトコルが定義されているインタフェースファイルをインポートするのではなく、@protocol ディレクティブを使って必要なプロトコルを前方参照する必要があります。次のコードの抜粋は、これをどのように行うかを示しています。
|
@protocol ディレクティブをこのように使用すると、「B」が後で定義するプロトコルであることがコンパイラに単純に通知されます。プロトコル B が定義されているインタフェースファイルはインポートしません。
| < 前ページ次ページ > |
Last updated: 2003-09-16
製品のご購入・ご購入相談は、お気軽に アップルストアまで
0120-APPLE-1(0120-27753-1) Copyright © 2004 Apple Computer, Inc. All rights reserved. | Terms of use | Privacy Notice |