読者です 読者をやめる 読者になる 読者になる

Objective-Cの循環参照について

2年ほど前に書いた記事 #import の使いどころ:循環参照しないために。 が未だにそれなりにアクセスがあるのですが、これは間違いがあります。正確にいうと、間違いでは無いのですが、冗長であまり意味のない記述でした。

#import は C言語の #includeを拡張して、一度だけしかヘッダファイルを読み込まないようになっています。そのため基本的には循環参照は起こらないようになっています。

では、何が問題になるか。
先の記事のコードは、以下のようなものでした。

ClassA.h

#import "ClassB.h"

@interface ClassA {
  ClassB variableB;
}
@end

ClassB.h

#import "ClassA.h"

@interface ClassB {
  ClassA variableA;
}
@end

これをコンパイルすると、以下のような処理が行われます。

  1. ClassA.h を読み込む
  2. ClassA.h の先頭の#import 宣言により、ClassB.h を読み込む
    1. ClassB.h を読み込む
    2. ClassB.h の先頭の#import 宣言により、ClassA.h を読み込もうとするが、既に読み込み済みのためスキップする。
    3. @interface で ClassB を宣言
    4. 型 ClassA の 変数 variableA を宣言
    5. エラー!! 型 ClassA が見つからない!!

ClassB.h を読み込んだときに、まだClassAの宣言が見つからないため、このエラーが発生します。そこで、@class ディレクティブで、ClassA はクラスだということを教えてあげる必要があります。@class 宣言を追加した正しいコードは以下の通り。

ClassA.h

#import "ClassB.h"
@class ClassB;

@interface ClassA {
  ClassB variableB;
}
@end

ClassB.h

#import "ClassA.h"
@class ClassA;

@interface ClassB {
  ClassA variableA;
}
@end

実装ファイルとヘッダファイルにimportを分ける必要は特にありません。全部ヘッダファイルに書いてOKです。

もう1点。
そのクラスのメンバーにアクセスしない場合は、import宣言は特に不要です。メソッドの戻り値のように、実装ファイルの中でそのオブジェクトのメンバーにアクセスする必要が無い場合などは、@class でクラス宣言だけしてやればOKです。インスタンス変数やメソッド引数のような場合は、実装中で使用するはずですので、importは必要でしょう。

@class は、その型がクラスであることを示しているだけです。逆に言うと使用するクラスを全部 @class で宣言しておいても特に問題はありません。(めんどくさいので普通はやらない) 循環しそうだな−、というのは @class で宣言しておくと幸せになれるかもしれません。

ということで、まとめ。

  • 循環参照しそうな場合は、ヘッダに @class を宣言しとくと良い
  • 実装ファイルで別途 importする必要は無い。

こちらも是非参考に:
Objective-Cのクラスの前方宣言がないと困ること