[iOS][Objective-C] @property の基本まとめ

Objective-C でなんとなく知っているけど実はよく知らないプロパティ(@property)まわりの
基本的な仕様をまとめました。

Xcode6.1が正式リリースされてSwiftのβがとれたし、「Swift使うからいらないよ」なんて言わないで、iOS開発のお供にどうぞ。

プロパティ(@property)って何?

メンバ変数(インスタンス変数)を外部から参照、代入するためのアクセサ(getter/setter)です。
メッセージ([]カッコで囲むアレ)で独自のgetter/setterを実装、使用してもいいですが、
プロパティを使う事で foo.name = @”John”; のような他言語でも馴染みのあるドット区切りで参照・代入ができるようになります。

例:

// クラスヘッダファイル(Foo.h)

@interface Foo : NSObject

@property (nonatomic, copy) NSString *name;
 @property (nonatomic, assign) NSUInteger age;

@end 
// クラス実装(Foo.m)

#import "Foo.h"

@implementation Foo
@end 
// 利用側ソース
Foo *foo = [[Foo alloc] init];
foo.name = @"Hanako"; // 代入
foo.age = 18; // 代入
NSLog(@"name: %@ age:", foo.name, foo.age); // 参照

プロパティ宣言(@property)が何をしているか

プロパティ(@property)の宣言はコンパイル時にそのプロパティ名のgetter/setterコードに展開されます。
前項のコードは実際には以下のように宣言・定義するのと同等です。

// クラスヘッダファイル(Foo.h)

@interface Foo : NSObject

<ul>
<li>(NSString *)name;</li>
<li>(void)setName: (NSString  *)value;</li>
<li><p>(NSUInteger)age;</p></li>
<li>(void)setAge: (NSUInteger)value;</li>
</ul>

<p>@end 
// クラス実装(Foo.m)

#import "Foo.h"

@implementation Foo {
  NSString *_name;
  NSUInteger _age;
}

<ul>
<li>(NSString *)name {
return _name;
}</li></ul></p>
<li><p>(void)setName: (NSString  *)value {
// 註:@propertyでcopy属性(後述)を指定しているので、コピーした値を内部に持つ処理になっています。
_name = [value copy]; 
}</p></li>
<li><p>(NSUInteger)age {
return _age;
}</p></li>
<li>(void)setAge: (NSUInteger)value{
_age = value;
}</li>


<p>@end 

このように、実際にはプロパティはsetter/getterとして定義されています。
逆にいうと、この書式に従ったsetter/getterは@propertyとして宣言していなくてもプロパティのように参照・代入できるということです。

つまり、value = [foo age] と value = foo.age は同じで、
[foo setAge: 30] と foo.age = 30 は同じ処理です。

プロパティの実装、@synthesize

最初の例では省略しましたが、プロパティ(@property)の実装として、
クラス実装ファイルに 「@synthesize プロパティ名」を記述することもできます。

// クラス実装(Foo.m)

#import "Foo.h"

@implementation Foo

@synthesize name, age; // @property name と ageの実装。

@end 

Xcode4.3以前は上記のコードがないとプロパティは正しく動作しませんでした。

Xcode4.4(Apple LLVM compiler 3.0)以降は@synthesizeがないプロパティには自動で@synthesizeが補完されるようになったため、
@synthesizeというキーワードを明示しなくてもよくなりました。

@propertyはgetter/setterのヘッダ宣言を行うものであるのに対して@synthesizeは宣言に合わせたメッセージ実装・メンバ変数宣言に置き換えられるものです。

使用するメンバ変数名は「@synthesize プロパティ名 = メンバ変数名」という指定を行う事で変更する事も可能です。

// クラス実装(Foo.m)

@implementation Foo 
@synthesize name = _myName; // メンバー変数として _name の代わりに _myName を使う
@synthesize age = _myAge; // メンバー変数として _name の代わりに _myName を使う

<ul>
<li>(NSString *)description{
// @synthesizeで生成されたメンバ変数は直接参照も可能です
return [NSString stringWithFormat: @"name: %@ age: %zd", _myName, _myAge];
}
@end 

@propertyに指定できる属性

属性とは

@property (nonatomic, copy) のように、カッコ内に指定するキーワードを属性といいます。
属性は自動生成されるgetter/setterの挙動を定義するもので、

  • アトミック性を決める属性
  • 利用するアクセサの種類や名前を決める属性
  • メモリ管理属性

の3タイプの属性があります。

アトミック性属性
アトミック性を決める属性は2種類しかありません。

atomic
省略時のデフォルト。synchronizedでスレッドセーフにしたgetter,setterを生成します。
nonatomic
スレッドセーフでない単純なgetter,setterを生成します。iOSではパフォーマンスの問題からほとんどの場合nonatomicにすべきです。

アクセサ属性

readwrite
デフォルト。getter/setter両方を生成します。
readonly
getterのみ生成します。
geter=メソッド名
getterの名前を設定する。デフォルトはプロパティ名と同じ。BOOL 型のプロパティの名前を is〜とすると分かりやすくなります。
seter=メソッド名
setterの名前を設定する。デフォルトは「setプロパティ名」。あまり使われることはありません。

メモリ管理属性

メモリ管理属性はARCが有効かどうか、Objective-Cのオブジェクト(NSObject)かどうかによりデフォルトが異なります。

assign
非オブジェクト型のデフォルト。NSIntegerなど値型、C由来の型など、メモリ管理が必要ないものに使用します。オブジェクト型の場合はassignでなくweakかunsafe_unretainedが推奨されます。
strong
オブジェクト型のARC環境デフォルト。setterで代入前のものをreleaseし、新たに代入するものをretainします。非オブジェクト型には使用できません。
retain
strongと全く同じです(参考:Transitioning to ARC Release Notes)
weak
オブジェクト型の非ARC環境デフォルト。代入時にretain/releaseを行いません。メモリアクセスエラーを避けるため参照先が解放された時はnilになります。strongと同様、非オブジェクト型には使用できません。
unsafe_unretained
weakと似ていますが、releaseされてもnilになりません。assignと同じですが、こちらはオブジェクト型を対象としています。
copy
retain/releaseする点はstrongと同じですが、代入時にsetterで引数のオブジェクトをretainせずに、新たなオブジェクトとしてコピー(_value = [value copy])してからretainします。用途としてはNSStringなどmutableな型(NSMutableString)が子クラスとして存在するものについて、代入後に外部からの値の変更を防ぐためにstrongの代わりにcopyを使用します。

プロパティのカスタム実装

プロパティは自動的な実装に任せず、任意の実装にすることも可能です。

カスタムのgetter/setterを実装する場合、プロパティのアトミック性属性指定やメモリ管理属性指定を無視した実装も可能ですが、
利用側からは属性に合わせた実装を期待されるため、属性による契約にしたがって適切に実装することが大切です。

// クラスヘッダファイル(Foo.h)

@interface Foo : NSObject

@property (nonatomic, copy) NSArray *source;

@property (nonatomic, readonly) NSInteger count;

@property (nonatomic, readonly, getter=isActive) BOOL active;

@end 
// クラス実装(Foo.m)

#import "Foo.h"

@implementation Foo{
  NSMutableArray *_storage;
}

// sourceのgetter実装
-(NSArray *)source{
  return _storage;
}

// sourceのsetter実装
-(void)setSource: (NSArray *)source{
  _storage = [[NSMutableArray alloc] initWithArray: source]; // NSMutableArrayにするのでcopyは不要
}

// countのgetter実装
- (NSInteger) count {
  return [_storage count];
}

// activeのgetter実装
- (BOOL) isActive{
  return [_storage count] > 0;
}
@end 

同一クラスからのプロパティ使用

同一クラス内では参照はgetterではなくインスタンス直接参照、設定は代入方法の統一のためsetterを使用することが推奨されます。

例外として、initメソッドではproperty代入(setter)を使うべきではありません。
setterは子クラスがオーバーライドする可能性があり、init内での初期化値(空文字など)を許容するとは限らないためです。

// 親クラス
@interface Parent: NSObject

@property (nonatomic) NSString *name;

@end

@implementation Parent

-(id)init{
  [super init];
  _name = @"john"; 
  // self.name = @"john"; // プロパティに代入するとChildで正しく初期化されなくなる。
}

@end

// 子クラス
@interface Child: Parent
@end

@implementation Child
-(void)setName:(NSString *)name{
    // バリデーションチェックでNGの場合はセットしない
  if(!name || [name isEqualToString:@""]){
    [NSException raise: NSInvalidArgumentException format:@"name must not be empty"];
  }
  // 1文字目は英大文字のみ許容する
  char firstChar = [name characterAtIndex:0];
  if(firstChar < 'A' || firstChar > 'Z'){
    [NSException raise: NSInvalidArgumentException format:@"name must start with large character."];
  }
  _name = name;
}
@end

getterを使うべき例外としては、コストの高いインスタンスを遅延初期化する場合など、特別な契約がある場合です。

@interface Foo: NSObject

@property (nonatomic, readdonly) HeavyObject *object; // 何か大きなオブジェクト

@end

@implementation Foo{
  HeavyObject *_object;
}

-(void)doSomething{
  // NSLog(@"%@",_object); // 初期化されていないおそれがある
  NSLog(@"%@",self.object); // こちらのほうが安全
}

-(HeavyObject *)object{
  if(!_object){
    _object = [[HeavyObject alloc] init];
  }
  return _object;
}

@end

おまけ – IBOutlet と IBAction

Xcodeを使っていると勝手にプロパティにIBOutletがついたり、メッセージにIBActionがついたりします。

@interface Foo : NSObject
@property (nonatomic) IBOutlet UIButton *button;

-(IBAction)buttonTapped:(id)sender;
@end

これらは.storyboard, .xibなどのIB(Interface Builder)と接続しているプロパティ(IBOutlet)、メッセージ(IBAction)を表すキーワードです。

IBOutlet, IBActionともにインターフェイスビルダーからコードを生成した時に自動で設定されますが、
実際にはIBOutletはプログラム上は何の意味もなく、IBActionはvoidの別名になっています。

2つとも、UIKitフレームワークのUINibDeclarations.hに定義されています。

//
//  UINibDeclarations.h
//  UIKit
//
//  Copyright (c) 2005-2014 Apple Inc. All rights reserved.
//

#ifndef IBOutlet
#define IBOutlet
#endif

#ifndef IBOutletCollection
#define IBOutletCollection(ClassName)
#endif

#ifndef IBAction
#define IBAction void
#endif

#ifndef IBInspectable
#define IBInspectable
#endif

#ifndef IB_DESIGNABLE
#define IB_DESIGNABLE
#endif

このようにdefineで定義されているだけですので、
冒頭のコードは以下のように IBOutlet → (削除)、IBAction → voidに置き換えたものと等価です。

@interface Foo : NSObject
@property (nonatomic) UIButton *button;

-(void)buttonTapped:(id)sender;
@end

プログラム上の意味はありませんが、人の可読性を高めるためにもNIB(ストーリーボードやXIB)と連動しているプロパティ、メッセージにはIBOutletやIBActionを明示するほうがよいでしょう。

最後に

Xcode や AppCode が自動生成、補完してくれるのであまりプロパティ、メンバ変数、メッセージ等の仕様をそれほど気にしなくても書けるようになってきていますが、
Objective-Cの基本を抑えてどう動いているのか理解することできれいな不具合のないコードを書く助けになるでしょう。

この記事は書籍 Effective Objective-C 2.0 を参考にしています。
さらに Objective-C の事を知るためにおすすめの一冊です。