iOSにおける@propertyの仕組みと属性修飾子の完全解説

@propertyの本質

@propertyはアクセサメソッド(getter/setter)の宣言であり、コンパイラが自動的にアクセサとバッキングインスタンス変数(ivar)を合成します。ドット構文での呼び出しも可能になります。

例:

@property (nonatomic) NSInteger userAge;

コンパイラは以下と等価なコードを自動生成します:

{
    NSInteger _userAge;
}
- (NSInteger)userAge {
    return _userAge;
}
- (void)setUserAge:(NSInteger)newValue {
    _userAge = newValue;
}

主な利点:

  1. ボイラープレートコードの削減
  2. 所有権の明示
  3. パフォーマンスと並行性の制御
  4. KVC/KVOとの親和性
  5. 可読性と保守性の向上

属性修飾子の分類

読み書き属性

  • 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段階で説明できます。

  1. 代入段階:copy修飾されたNSMutableString型プロパティに代入する際、対象オブジェクトは「深いコピー」され、イミュータブルなNSStringに変換される。したがって、プロパティself.pStrは実際にはNSString(イミュータブルオブジェクト)を指している。
  2. コピーセグメント:イミュータブルオブジェクトに対して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の違い

  1. ivarは純粋なストレージである。
  2. propertyはそのストレージにアクセスするための「メソッドインターフェース」である。
  3. ほとんどの場合self.ageを使用するが、init/dealloc内やカスタムアクセサの内部では再帰等の問題を避けるため_ageで直接アクセスすることが多い。

タグ: Objective-C iOS @property メモリ管理 Cocoa Touch

8月9日 17:49 投稿