Objective-Cのクラスの前方宣言がないと困ること

クラス名の前方宣言について質問をもらったので少しまとめます。
Objective-Cでクラス名を前方宣言したいときには「@class」文を使用します。例えば「MyObject」という名前のクラスであれば、次のように書きます。
[cc lang=”objc”]
@class MyObject;
[/cc]
ヘッダファイルの中で、Objective-Cのクラスのインスタンスをメンバー変数にしたいときはクラスが存在することを知らなければいけません。このようなときに「#import」文をヘッダファイルに書いてクラスの宣言を読み込みます。例えば次のようなコードです。(このヘッダファイルを「MyObjectB.h」ファイルとします)
[cc lang=”objc”]
#import “Cocoa/Cocoa.h”
#import “MyObject.h”
@interface MyObjectB : NSObject {
MyObject *_myObject;
}
@end
[/cc]
このとき、もしも「MyObject.h」ファイルに次のようなコードが書かれていると問題が起こります。
[cc lang=”objc”]
#import “Cocoa/Cocoa.h”
#import “MyObjectB.h”
@interface MyObject : NSObject {
MyObjectB *_object;
}
@end
[/cc]
このコードをビルドしようとすると、コンパイラは次のようなエラーを表示します。
[cc]
Complie MyObject.m
Expected specifier-qualifer-list before “MyObject”
Compile MyObjectB.m
Expected specifier-qualifier-list before “MyObjectB”
[/cc]
エラーメッセージの意味は次の2つです。

  • 「MyObject.m」ファイル(「MyObject.h」ファイルを#importしている)のビルドで、「MyObject」の定義が無い。
  • 「MyObjectB.m」ファイル(「MyObjectB.h」ファイルを#importしている)のビルドで、「MyObjectB」の定義が無い

「#import」文で、それぞれのクラスのインターフェイスを読み込んでいるのになぜ「定義が無い」といわれてしまうのでしょうか?
実は、ビルドのときにプリプロセッサによってどのような処理が行われているかを考えてみると原因が分かります。プリプロセッサはビルドするときに「#import」文などを実行して、次のような動きをします。(「Cocoa/Cocoa.h」ファイルはここでは関係ないので、その内容は割愛します)
「MyObject.h」ファイルの処理(「MyObject.m」ファイルのコンパイル)

  1. 「Cocoa/Cocoa.h」ファイルを読み込む
  2. 「MyObjectB.h」ファイルを読み込む(この時点では「MyObject」クラスは定義されていない
  3. 「MyObjectB.h」ファイルの中の「#import “MyObject.h”」文でファイルの読み込みを行おうとする。しかし、「#import」文では一回しかヘッダファイルは読まれないので、「MyObjectB.h」ファイルの中の「#import “MyObject.h”」文は無視される。
  4. 「MyObjectB」クラスの中の「MyObject」クラス型のメンバー変数の定義文で「MyObject」クラスの宣言が、まだ、無いからエラー(この時点では2の実行中なので、まだ、「MyObject」クラスの宣言まで行っていません)

「MyObjectB.h」ファイルの処理(「MyObjectB.m」ファイルのコンパイル)

  1. 「Cocoa/Cocoa.h」ファイルを読み込む
  2. 「MyObject.h」ファイルを読み込む
  3. 「MyObject.h」ファイルの中の「#import “MyObjectB.h”」文でファイルの読み込みを行おうとする。しかし、「#import」文では一回しかヘッダファイルは読み込まれないので、「MyObject.h」ファイルの中の「#import “MyObjectB.h”」文は無視される。
  4. 「MyObject」クラスの中の「MyObjectB」クラス型のメンバー変数の定義分で「MyObjectB」クラスの宣言が、まだ、無いからエラー(この時点では2の実行中なので、まだ、「MyObjectB」クラスの宣言まで行っていません)

このような、循環インクルード(循環インポート)における問題を防止するために使用するのが「@class」文です。上記の2つのコードを次のように修正します。
「MyObject.h」ファイル
[cc lang=”objc”]
#import “Cocoa/Cocoa.h”
//#import “MyObjectB.h”
@class MyObjectB;
@interface MyObject : NSObject {
MyObjectB *_object;
}
@end
[/cc]
「MyObjectB.h」ファイル
[cc lang=”objc”]
#import “Cocoa/Cocoa.h”
//#import “MyObject.h”
@class MyObject;
@interface MyObjectB : NSObject {
MyObject *_myObject;
}
@end
[/cc]
このようにすると、お互いのヘッダファイルを参照しないので、循環する恐れが無く、クラスが存在することがわかっているのでメンバー変数も定義できます。
同じようにプロトコルも前方宣言ができます。次のようなコードです。
[cc lang=”objc”]
@protocol ProtoclName;
[/cc]
デリゲートメソッドをプロトコルで定義したときなど、クラスを先に書くか、プロトコルを先に書くかのどちらを行ってもお互いを知らないからエラーになるのを防止できます。
ただし、一つ注意。前方宣言では名前のみなので、インスタンスを確保する(「alloc」メソッドを呼ぶとき)とか、クラスを継承するとかのときには使えません。サブクラスを定義するときには必ず親クラスの定義が必要です。ですので、ヘッダファイルでは親クラスのヘッダファイルの読み込みは必須です。
それと、前方宣言することでのメリットは循環インクルード(循環インポート)の防止だけではなくコンパイル時間の削減にも効果あります。ヘッダファイルが大量のヘッダファイルをインポートしていて、インポートされているヘッダファイルも別のヘッダファイルを…などとなっていくると、ビルド時間が長くなっていきます。前方宣言ならそのようなことが起きないので、ビルド時間が指数関数的に増えるということはなくなります。もちろん、インクルードではなく、そもそものコードが膨大な場合にはビルド時間がかかるというのはかわりません。また、ヘッダファイルに依存関係が凄まじいと1つファイルを変更すると全体フルビルドみたいな状況になるので、できるだけヘッダファイルの依存関係を減らしておくというのは精神衛生上もよいことだと思います。
検証に使ったプロジェクトファイル(上記のコードが書いてあるだけですが)はこちらからダウンロードできます。ビルドできる状態のコードになっているので、「#import」文のコメントアウトを解除して、「@class」文をコメントアウトしてからビルドしてみてください。エラーになります。(Mac OS X 10.6 + Xcode 3.2.6で確認しました)
サンプルコードのダウンロード
RecursiveImport

関連記事

  1. MultiTextConverter 3.5.5を公開しました

  2. 明けましておめでとうざいます

  3. Webサイト全体をHTTPS化

  4. VMware FusionでSierraを動かすときはメモリ設定に注意…

  5. 課題をこなすリズム

  6. Meta Packageの作成環境

最近の著書

  1. 基礎から学ぶ SwiftUI

最近の記事

  1. 基礎から学ぶ SwiftUI
  2. 基礎から学ぶ SwiftUI