@propertyの本質
@propertyはアクセサメソッド(getter/setter)の宣言であり、コンパイラが自動的にアクセサとバッキングインスタンス変数(ivar)を合成します。ドット構文での呼び出しも可能になります。
例:
@property (nonatomic) NSInteger userAge;
コンパイラは以下と等価なコードを自動生成します:
{
NSInteger _userAge;
}
- (NSInteger)userAge {
return _userAge;
}
- (void)setUserAge:(NSInteger)newValue {
_userAge = newValue;
}
主な利点:
- ボイラープレートコードの削減
- 所有権の明示
- パフォーマンスと並行性の制御
- KVC/KVOとの親和性
- 可読性と保守性の向上
属性修飾子の分類
読み書き属性
- readwrite:読み書き可能(デフォルト)
- readonly:読み取り専用
原子性
- atomic:単一のアクセサ呼び出しに対して原子性を保証するが、低速。デフォルト値。
注意:atomicは遅く、一連の操作のスレッド安全性までは保証しない。順序、トランザクション、オブジェクト内部の並行安全性も含まれないため、実運用では明示的な同期化が必要。 - nonatomic:同期を行わず、高速。
メモリ管理修飾子
- strong:強い参照を保持し、参照カウントを+1する。新しい値はretain、古い値はrelease。
用途:一般的なオブジェクト所有、親から子への参照。 - weak:所有せず、参照カウントを変化させない。オブジェクト解放時にポインタがnilになる。
用途:循環参照の回避。delegate、子から親への参照、IBOutlet等。
注意:アクセス時に既にnilになっている可能性がある。 - copy:イミュータブルなコピーを生成し、setter内で
-copyを呼び出す。
用途:ミュータブルオブジェクトの事後変更を防ぐ、blockのヒープコピー等。 - assign:ビットコピーを行い、参照カウントを変化させない。
用途:スカラー型や構造体。
注意:オブジェクトポインタにassignを使うとダングリングポインタが発生する。 - unsafe_unretained:所有せず、参照カウントを変化させない。オブジェクト解放時にnil化されない。
用途:weakが利用できなかった時代のレガシー修飾子。
その他
- getter=isEnabled / setter=setEnabled::カスタムアクセサ名の指定。
- class:クラスプロパティ。
copy修飾子の深掘り
strongまたはcopyで修飾されたミュータブル/イミュータブルなオブジェクトに対し、代入やコピーを行った際の挙動を以下の表にまとめます。
| プロパティ型 | -copy | -mutableCopy | NSString代入 | NSMutableString代入 |
|---|---|---|---|---|
| (copy) NSString | 浅いコピー | 深いコピー | 浅いコピー | 深いコピー |
| (strong) NSString | 浅いコピー | 深いコピー | 浅いコピー | 浅いコピー |
| (copy) NSMutableString | 浅いコピー | 深いコピー | 浅いコピー | 深いコピー |
| (strong) NSMutableString | 深いコピー | 深いコピー | 浅いコピー | 浅いコピー |
コピー操作の規則
-copyは常にイミュータブルなオブジェクトを返す。- 呼び出し元がイミュータブル型の場合:浅いコピー。
- 呼び出し元がミュータブル型の場合:深いコピーを行い、イミュータブル型のオブジェクトを得る。
-mutableCopyは常にミュータブルなオブジェクトを返す(必ず深いコピー)。
代入操作の規則
- strong修飾のプロパティへの代入:浅いコピーのみ。プロパティと代入オブジェクトの「可変性」は一致する。
- copy修飾のプロパティへの代入:深いコピーまたは浅いコピーのいずれかだが、結果は常に「イミュータブル」。
- 代入オブジェクトがイミュータブル型の場合:浅いコピー。
- 代入オブジェクトがミュータブル型の場合:深いコピーを行い、イミュータブル型のオブジェクトを得る。
疑問:なぜ[(copy)NSMutableString copy]は浅いコピーになるのか?
上記の結論に基づき、2段階で説明できます。
- 代入段階:copy修飾されたNSMutableString型プロパティに代入する際、対象オブジェクトは「深いコピー」され、イミュータブルなNSStringに変換される。したがって、プロパティ
self.pStrは実際にはNSString(イミュータブルオブジェクト)を指している。 - コピーセグメント:イミュータブルオブジェクトに対して
copy操作を行うと、ポインタレベルで同一のオブジェクトが返される。
以上より、[(copy)NSMutableString copy]は浅いコピー操作となる。
バッキングivarの生成ルール
ivarが生成されるケース
- クラスの
@interfaceまたはクラス拡張(extension)で@propertyを宣言した場合。 @dynamicを明示的に使用しておらず、setterとgetterの両方を手動で実装していない場合。
ivarが生成されないケース
- カテゴリで
@propertyを宣言した場合。 @dynamicを使用し、ランタイムがアクセサを提供することを約束した場合。- getterとsetterの両方を既に実装している場合。
- プロトコル(
@protocol)内で@propertyを宣言した場合。 - クラスプロパティ。
例外と詳細
readonlyの場合、getterを実装するとivarは自動合成されない。- クラスプロパティにはivarのインスタンスが存在せず、通常は
static変数等でストレージを実現する。
@interface AppConfig : NSObject
@property (class, nonatomic, copy) NSString *buildVersion;
@end
@implementation AppConfig
static NSString *_buildVersion;
+ (NSString *)buildVersion {
return _buildVersion;
}
+ (void)setBuildVersion:(NSString *)version {
_buildVersion = [version copy];
}
@end
- カテゴリのプロパティにストレージを持たせるには、関連オブジェクト(associated object)を使用する。
#import <objc/runtime.h>
@interface UIView (BadgeLabel)
@property (nonatomic, copy) NSString *badgeString;
@end
@implementation UIView (BadgeLabel)
static const void *kBadgeIdentifier = &kBadgeIdentifier;
- (void)setBadgeString:(NSString *)badgeString {
objc_setAssociatedObject(self, kBadgeIdentifier, badgeString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)badgeString {
return objc_getAssociatedObject(self, kBadgeIdentifier);
}
@end
@dynamic、@synthesize、計算プロパティ
@dynamic
コンパイラに対して、アクセサとivarの生成を不要と伝え、メソッド未実装の警告も抑制する。
用途:Core DataのNSManagedObjectサブクラス等。
@interface BookEntity : NSManagedObject
@property (nonatomic, copy) NSString *bookTitle;
@end
@implementation BookEntity
@dynamic bookTitle; // アクセサはランタイム(Core Data)が注入。コンパイラは生成も警告も行わない
@end
@synthesize
コンパイラに@propertyのgetter/setterとバッキングivarの生成を指示し、プロパティ名をカスタムivar名にマッピングする。
計算プロパティ
ストレージに依存せず、必要に応じて値を計算するプロパティ。
propertyとivarの違い
ivarは純粋なストレージである。propertyはそのストレージにアクセスするための「メソッドインターフェース」である。- ほとんどの場合
self.ageを使用するが、init/dealloc内やカスタムアクセサの内部では再帰等の問題を避けるため_ageで直接アクセスすることが多い。