Effective Objective-C 2.0 第2章(オブジェクト、メッセージング、ランタイム)メモ
項目6 プロパティの理解
- 宣言プロパティ(declared property)の属性
アトミック性
デフォルト(何も書かない場合)はatomic
-
- nonatomic 排他的に実行されないアクセッサメソッド。
- atomic アクセッサにアトミック性を保証するロック機能が付く。自分でを定義する場合は、そのためのコードを書かなければならない(書くべき)。
読み書き属性
デフォルトはreadwrite。@interface部でreadonlyにして、クラスエクステンションでreadwriteにすることもできる。
-
- readwrite
- readonly
メソッド名指定
指定しなければ、getter =
-
- getter=
isというプレフィックスをつけるために、論理値のプロパティに使われることが多い。 - setter=
あまり使われない。
- getter=
値の設定方法
デフォルトはNon-ARCならassign、ARCならstrong?
-
- copy オブジェクトをcopyして設定。プロパティのクラスは、NSCopyingプロトコルを採用し、copyメソッドが利用できる必要がある。NSString等mutableなサブクラスが存在する場合に、見えないところで値が書き換えられるのを防ぐために使用する。
- weak (ARC用)弱参照。新しい値がセットされるとき、その値はretainされず、古い値はreleaseされない。プロパティが指しているオブジェクトが破棄されたときnilに書き換えられる。対応するインスタンス変数は__weakで修飾されている必要がある。
- assign 単純な代入。CGFloatやNSIntegerといった非オブジェクト型に使う。
- unsafe_unretained (ARC用)assignと同じだがターゲットがオブジェクト型。新しい値がセットされるとき、その値はretainされず、weakとは異なり破棄されたときに値がnilに書き換えられない。対応するインスタンス変数は__unsafe_unretained修飾されている必要がある。
- retain 新しい値がセットされるとき、まずその値をretainし、古い値をreleaseしてから、新しい値をセットする。
- strong (ARC用)retainと同じ。対応するインスタンス変数は、ライフタイム修飾子で修飾されていないか、___strongで参照されている必要がある。
- 自分でアクセッサを定義する場合は、指定したプロパティ属性に沿った実装をする。
- (void)setVal:(TYPE)obj { //retain指定した場合のセッタ例 if (_val != obj) { [_val release]; _val = [obj retain]; } } - (void)setVal:(TYPE)obj { //copy指定した場合のセッタ例 if (_val != obj) { [_val release]; _val = [obj copy]; } }
@dynamic指定すれば、自動合成はされない。
CoreDataのNSManagedObjectをサブクラス化するとき等に使われるプロパティがインスタンス変数ではないため。
@interface Status : NSObject @property(getter = HP, setter = setHP:) int hitPoint; @property(getter = MP, setter = setMP:) int magicPoint; @property/*(readonly)*/int level; @end @implementation Status @dynamic level; - (int)level { return (_hitPoint + _magicPoint)/2; // (self.HP + self.MP) / 2; } - (NSString*)description { return [NSString stringWithFormat:@"HP = %d, MP = %d, Lv = %d", _hitPoint, _magicPoint, self.level //@dynamic指定しているため_levelはない。自分で定義したゲッタメソッドを使う。 ]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Status *st = [[Status alloc] init]; st.HP = 10; //[st setHP:10]; st.MP = 5; //[st setMP:10]; NSLog(@"%@", st); //[st setLevel:10]; //setLevel:を実装していないので実行時エラー. readonly指定していればコンパイルエラー } return 0; }
- @property構文でアクセッサメソッドを自動合成できる
以前は、@synthesize構文を書かないと自動合成されなかったが、今は不要。
自動で生成されるインスタンス変数の名前を指定したい場合ぐらいにしか@synthesizeは使わない。
自動生成されるインスタンス変数はプロパティで指定した名前の頭に_が付く。
@synthesize firstName = _myFirstName;
- iOSでは、atomicを使うとパフォーマンスが大きく損なわれるので、nonatomicを使う。
通常、アトミック性を保証しただけなら、他のスレッドが同時書き込みをしているときに、同じプロパティ値を連続して何度も読み出すと、別の値が返される(スレッドセーフではない)。
項目7 インスタンス変数にクラス内でアクセスするときは直接アクセスする
※ オブジェクトの外からインスタンス変数にアクセスするときには、必ずプロパティを使うようにすべきだが、クラス内でインスタンス変数にアクセスするときにはどうすべきかは、色々な意見がある。
あくまで、Effective Objective-Cでは、↓のようなポリシーを推奨しているというだけ。
- クラス内での読み出しではインスタンス変数の直接読み出し、書き込みではプロパティを介した書き込みを使う。
- 直接アクセスの方が高速。
- 直接アクセスはメモリ管理属性を参照しないので注意する。copy, retain, release etc.
- 直接アクセスではKVO(Key Value Observing)通知は生成されない。(問題になる場合もそうでない場合もある)
- イニシャライザとdeallocでは、かならずインスタンス変数を介して直接データを読み書きする。
サブクラスで想定外のオーバーライドがあるかもしれないので直接アクセスした方が良いらしい。
ただし、インスタンス変数がスーパークラスで宣言されている場合は、アクセッサを使わないといけない。
うーーん、インスタンスが遅延初期化する場合とかもあるし、メモリ管理属性とか気にしないと行けないし、どうせ全て直接アクセスできないなら、イニシャライザで気をつけるより、アクセッサをオーバーライドする側で気をつけた方が良いんじゃないかな?
@interface Name : NSObject @property(nonatomic, copy) NSString *firstName, *lastName; @end @implementation Name - (instancetype)init { if (self = [super init]) self.firstName = self.lastName = @""; return self; } @end @interface Yamada : Name @end @implementation Yamada - (void)setLastName:(NSString *)lastName { if (![lastName isEqualToString:@"Yamada"]) [NSException raise:NSInvalidArgumentException format:@"Last name must be Yamada"]; self.lastName = lastName; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Name *yamada = [Yamada new]; //必ず例外発生 } return 0; }
- データ遅延初期化されている場合は、プロパティを介してデータを読み出さなければならない場合がある。
セットアップコストがかかり、アクセス頻度が低い場合など、ゲッタで初期化を先延ばしする場合。
- (TYPE)val { if (!_val) _val = [TYPE new]; //重い初期化処理 return _val; }
項目8 オブジェクトが等しいとはどういうことかを理解しよう
- 同値比較したいオブジェクトでは、isEqual:とhashの2つのメソッドを用意する。
- 「2つのオブジェクトが等しい同じhash値」を守る(逆が成り立つ必要は無い)。
- hashは、高速に計算でき、衝突が起きる可能性が低くなるように実装する。
@interface Type : NSObject @property (nonatomic, copy) NSString *str; @property (nonatomic, retain) T *obj; @property (nonatomic) NSUInteger uintVal; @end @implementation Type //実装例 - (BOOL)isEqualToType:(Type*)o { if (self == o) return YES; if (![_str isEqualToString:o.str] || ![_obj isEqual:o.obj] || _uintVal != o.uintVal) { //一意な識別子があればそれを使って浅い同値比較をしても良い return NO; } return YES; } - (BOOL)isEqual:(id)object { //サブクラスを許容する場合は[self class] == [object class]を適当に書き換える return [self class] == [object class] ? [self isEqualToType:(Type*)object] : [super isEqual:object]; } - (NSUInteger)hash { return [_str hash] ^ [obj hash] ^ _uintVal; } @end
- コレクションにオブジェクトを追加したら、そのオブジェクトのハッシュ値が変更されることがあってはならない。
NSMutableSet *set = [NSMutableSet new]; NSMutableArray *arrayA = [@[@1,@2] mutableCopy]; [set addObject:arrayA]; NSLog(@"set = %@", set); //set = {((1,2))} NSMutableArray *arrayB = [@[@1,@2] mutableCopy]; [set addObject:arrayB]; NSLog(@"set = %@", set); //set = {((1,2))} NSMutableArray *arrayC = [@[@1] mutableCopy]; [set addObject:arrayC]; NSLog(@"set = %@", set); //set = {((1),(1,2))} [arrayC addObject:@2]; NSLog(@"set = %@", set); //set = {((1,2),(1,2))} 同じ要素を複数含む集合ができてしまう NSSet *setB = [set copy]; NSLog(@"set = %@", setB); //set = {((1,2))} 同じ要素を複数含む集合ができるわけではない
項目9 実装の詳細を隠すために、クラスクラスタパターンを使う
- クラスクラスタ(class cluster):同じインターフェースをもち、同じ機能を提供する複数のクラスの集合体
- パブリッククラス(public class):クラスクラスタのインターフェースを表す公開された抽象クラス
- プリミティブメソッド(primitive method):クラスクラスタにおいて、具体的なデータ構造やアルゴリズムに基づいて定義されるメソッド。パブリッククラスではサブクラスでプリミティブメソッドが実装されることを前提に、それ以外のメソッドが実装されている。
クラスクラスタのインスタンスのチェックには気をつける。ほとんどのコレクションがクラスクラスタ。
NSStringFromClass()でインスタンスのクラス名を確認できる。
void printClass(Class clazz, id ins) { printf("class = %s, \tisMemberOfClass = %s, \tisKindOfClass = %s\n", [NSStringFromClass([ins class]) UTF8String], [ins isMemberOfClass:clazz] ? "YES" : "NO", [ins isKindOfClass:clazz] ? "YES" : "NO" ); } int main(int argc, const char * argv[]) { @autoreleasepool { NSString *str = @"str"; Class clazz = [NSString class]; printClass(clazz, str); printClass(clazz, [str stringByAppendingString:@"ヽ(`Д´)ノ"]); printClass(clazz, NSHomeDirectory()); } return 0; }
class = __NSCFConstantString, isMemberOfClass = NO, isKindOfClass = YES class = __NSCFString, isMemberOfClass = NO, isKindOfClass = YES class = NSPathStore2, isMemberOfClass = NO, isKindOfClass = YES
クラスクラスタを拡張するにはカテゴリを使うのが手っ取り早い。サブクラス化する場合にはいくつか注意点がある。
クラスクラスタのパブリッククラスをサブクラス化する方法
項目10 既存のクラスにカスタムデータを追加するにはAssociated Objectを使う
- Associated Objectは、見つけにくいバグを埋め込みやすいので、ほかのアプローチでは不可能なときに使うようにしよう。
通常、オブジェクトに情報を追加したい場合は、クラスのサブクラス化を検討するが、
何か特別な手段でインスタンスが作られていて、自由にインスタンスを作るように指示することができない場合、
連想(関連)参照(associated references)を使う。
カテゴリではメソッドの追加だけで、インスタンス変数を追加することはできないので、連想参照が役立つ。
effective Objective-Cでは、UIAlertViewにボタンをタップした場合の操作をブロックオブジェクトにして、関連づけする例が示されている(delegateだとコードが分断されるため)。
※UIAlertController(iOS8以降)だと、blockオブジェクトを受け取るメソッドが用意されている。
#import <objc/message.h> //id object: オーナー、 id value: 参照オブジェクト void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) id objc_getAssociatedObject(id object, const void *key) void objc_removeAssociatedObjects(id object)
NSDictionaryのように使えるが、keyは単なるポインタ比較で区別する(isEqual:ではない)ので、staticなグローバル変数が使われることが多い(例えば static char kAssociated;としておいて、&kAssociatedを渡す).
policyにはOBJC_ASSOCIATION_RETAINといった@property属性と同じようなストレージポリシーを指定する。
objc_getAssociatedObject()で関連付けがなければnilを返す
参照の解除にobjc_removeAssociatedObjects()を使うと、指定したオーナーに関連づけられている全ての参照を解除してしまうので、通常はobjc_setAssociatedObject()でnilをセットして、個別に参照の解除を行う。
項目11 objc_msgSendの役割を理解する
送られたメッセージ(レシーバー、セレクタ、引数)は、すべて動的メッセージディスバッチシステムを通して実装をルックアップして実行する。
id returnValue = [reciever selector:arg]; id returnValue = objc_msgSend(reciever @selector(selector:), arg); //objc_msgSend family: objc_msgSend_stret(struct用), objc_msgSend_fpret(浮動小数点用), objc_msgSendSuper(super用) etc.
項目12 メッセージの転送を理解する
オブジェクトが未知のセレクタを検出したとき、
1. 動的メソッド解決で、クラスに実行時にメソッドを追加できる。
2. 自分が理解できない一定のセレクタをほかのオブジェクト(代替レシーバ)が処理できると宣言できる
3. セレクタの処理方法がどうしても見つからない場合は、本格的な転送メカニズムが実行される。
でメッセージ転送が行われる。
- 動的メソッド解決
オブジェクトが理解できないメッセージがオブジェクトに渡されたとき、最初に呼びさされるのは、
インスタンスメソッドの場合はresolveInstanceMethod:、
クラスメソッドの場合はresolveClassMethod:。
#import <Foundation/Foundation.h> @interface AddMethodSample : NSObject @property NSString *str; @property NSNumber *num; @property NSDate *date; @property id opaqueObject; @end
#import "AddMethodSample.h" #import <objc/runtime.h> @interface AddMethodSample () @property NSMutableDictionary *propertyData; @end id dicGetter(id self, SEL _cmd) { return [((AddMethodSample*)self).propertyData objectForKey:NSStringFromSelector(_cmd)]; } void dicSetter(id self, SEL _cmd, id value) { NSMutableString *key = [NSStringFromSelector(_cmd) mutableCopy]; AddMethodSample *typedSelf = (AddMethodSample*)self; //setHoge:をhogeに変えてキーにする [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)]; [key deleteCharactersInRange:NSMakeRange(0, 3)]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:[[key substringToIndex:1] lowercaseString]]; if (value) [typedSelf.propertyData setObject:value forKey:key]; else [typedSelf.propertyData removeObjectForKey:key]; } @implementation AddMethodSample @dynamic str, num, date, opaqueObject; - (id)init { if (self = [super init]) _propertyData = [NSMutableDictionary new]; return self; } + (BOOL)resolveInstanceMethod:(SEL)sel { [NSStringFromSelector(sel) hasPrefix:@"set"] ? class_addMethod(self, sel, (IMP)dicSetter, "v@:@") : class_addMethod(self, sel, (IMP)dicGetter, "@@:"); return YES; } @end
class_addMethod(Class cls, SEL name, IMP imp, const char *types)でメソッドを追加できる。
IMPは隠し引数(hidden arguments)(レシーバ、セレクタ)を含む関数へのポインタ(第一引数id self, 第二引数SEL _cmd)。tyesは、Type Encodings。
- 代替レシーバ
動的メソッド解決されなかった(resolveInstance(Class)MethodでNOを返した)場合、代替レシーバがあるかどうかを
forwardingTargetForSelector:で聞いてくる。処理する物が無い場合はnilを返す。
-(id)forwardingTargetForSelector:(SEL)aSelector { if ([self.delegate respondsToSelector:aSelector]) { return self.delegate; } return [super forwardingTargetForSelector:aSelector]; }
- 本格的な転送メカニズム
forwardingTargetForSelector:で処理されなかった(nilを返した)場合、forwardInvocation:(NSInvocation*)invocationが呼ばれる。NSInvocationは、設定されているselector, 設定されているtargetを設定し直したりできる。
NSInvocation#invokeWithTarget:で引数のオブジェクトをターゲットとして、レシーバの表すメッセージを送信する。メッセージの結果は、元のセンダに返される。
- (void)forwardInvocation:(NSInvocation *)anInvocation { [self.delegate respondsToSelector:anInvocation.selector] ? [anInvocation invokeWithTarget:self.delegate] : [super forwardInvocation:anInvocation]; }
処理しない場合は、スーパークラスの実装を呼び出さなければならない。NSObjectのforwardInvocation:が呼びさされると、doesNotRecognizeSelector:が呼び出され、未処理セレクタ例外(NSInvalidArgumentException)を発生させる。
ランタイムシステムが転送先のオブジェクトの情報を使ってNSInvocationのインスタンスを作成できるようメソッドシグネチャを返すメソッドmethodSignatureForSelector:を再定義しなければならない(あんまよく分かってない)。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [super respondsToSelector:aSelector] ? [super methodSignatureForSelector:aSelector] : [self.delegate methodSignatureForSelector:aSelector]; }
ちなみにdoesNotRecognizeSelectorを使えば、メッセージの使用を積極的に禁止できる.
- (void)setVal:(id)val { //使ってほしくないセッター [self doesNotRecognizeSelector:_cmd]; //_cmdはメソッドの隠し引数で、そのメソッドのセレクタを表す。 }
項目13 不透明なメソッドのデバッグではメソッドのSwizzlingを使うことを検討する
- Method Siwizzling:実行時に既存のメソッドの実装を、自前の実装に差し替える手法。通常はもとの実装に機能を追加するために使われる。
※Effective Objective-CではSwizzlingの使用はデバッグ時だけにすることを推奨している。
@interface NSString (LogString) - (NSString*)myLogStr_lowecaseString; @end @implementation NSString (LogString) - (NSString*)myLogStr_lowecaseString { NSString *lowercase = [self myLogStr_lowecaseString]; //(奇数回)swizzle()が呼ばれた後だとlowecaseStringの実装が呼ばれる NSLog(@"%@ -> %@", self, lowercase); return lowercase; } @end static void Swizzle(Class clazz, SEL aSel, SEL bSel) { Method m1 = class_getInstanceMethod(clazz, aSel), m2 = class_getInstanceMethod(clazz, bSel); method_exchangeImplementations(m1, m2); } int main(int argc, const char * argv[]) { @autoreleasepool { Swizzle([NSString class], @selector(lowercaseString), @selector(myLogStr_lowecaseString)); [@"abcdEFGHijkLMN" lowercaseString]; //swizzlingされているのでmyLogStr_lowecaseStringの実装が呼ばれる //出力:abcdEFGHijkLMN -> abcdefghijklmn } return 0; }
項目14 クラスオブジェクトとは何かを理解する
[SomeClass instance] | -isa-> | [SomeClass class] | -isa-> | [SomeClass metaclass] |
| | | | |||
super_class | super_class | |||
↓ | ↓ | |||
[NSObject class] | -isa-> | [NSObject metaclass] |
- オブジェクトはメッセージの転送を使っている可能性があるので、可能な限りクラスオブジェクトを直接比較するのではなく、イントロスペクションメソッドを使う。