これは本当に素晴らしい質問だと思います。本当の質問に取り組むのではなく、ほとんどの回答が問題を回避し、単純にスウィズリングを使用しないと言ったのは残念です。
焼けるように暑い方法を使用すると、キッチンで鋭いナイフを使用するようなものです。一部の人々は彼らが自分自身をひどく切るだろうと思うので鋭利なナイフを怖がっていますが、真実は鋭利なナイフがより安全であるということです。
メソッドのスウィズリングは、より優れた、より効率的な、より保守可能なコードを記述するために使用できます。また、悪用されて恐ろしいバグにつながる可能性があります。
バックグラウンド
すべてのデザインパターンと同様に、パターンの結果を十分に認識している場合は、パターンを使用するかどうかについて、より多くの情報に基づいた決定を行うことができます。シングルトンはかなり物議を醸す何かの良い例であり、正当な理由で—シングルトンは適切に実装するのが本当に難しいです。ただし、多くの人はまだシングルトンを使用することを選択しています。スウィズリングについても同じことが言えます。良い点と悪い点の両方を十分に理解したら、あなた自身の意見を形成するべきです。
討論
メソッドのスウィズリングの落とし穴のいくつかを以下に示します。
- メソッドのスウィズリングはアトミックではありません
- 所有されていないコードの動作を変更します
- 考えられる名前の競合
- スウィズリングはメソッドの引数を変更します
- スウィズルの順序は重要です
- 理解しにくい(再帰的に見える)
- デバッグが難しい
これらの点はすべて有効であり、それらに対処することで、メソッドのスウィズリングの理解と結果を達成するために使用される方法の両方を改善できます。一度に1つずつ取ります。
メソッドのスウィズリングはアトミックではありません
同時に使用しても安全なメソッドswizzlingの実装はまだ見ていません1。これは、メソッドのスウィズリングを使用する場合の95%の場合、実際には問題ではありません。通常、メソッドの実装を置き換えるだけで、その実装をプログラムの存続期間全体にわたって使用する必要があります。これは、メソッドの入れ替えをで行う必要があることを意味します+(void)load
。load
クラスメソッドは、アプリケーションの開始時に逐次実行されます。ここでスウィズルを実行しても、同時実行性に問題はありません。+(void)initialize
ただし、でスウィズルすると、スウィズリング実装で競合状態が発生し、ランタイムが奇妙な状態になる可能性があります。
所有されていないコードの動作を変更します
これはスウィズリングの問題ですが、それは全体の要点です。目標は、そのコードを変更できるようにすることです。これが大きな問題であると人々が指摘NSButton
するのは、変更したいインスタンスの1つだけを変更するのではなくNSButton
、アプリケーションのすべてのインスタンスを変更するためです。このため、スウィズルするときは注意が必要ですが、完全に回避する必要はありません。
このように考える...クラスのメソッドをオーバーライドし、スーパークラスのメソッドを呼び出さないと、問題が発生する可能性があります。ほとんどの場合、スーパークラスはそのメソッドが呼び出されることを期待しています(特に記載されていない限り)。これと同じ考えをスウィズリングに適用すれば、ほとんどの問題をカバーできました。常に元の実装を呼び出します。そうでない場合は、安全を確保するために変更しすぎている可能性があります。
考えられる名前の競合
名前の競合は、Cocoa全体で問題となっています。私たちは頻繁にカテゴリのクラス名とメソッド名に接頭辞を付けます。残念ながら、名前の衝突は私たちの言語の問題です。ただし、スウィズリングの場合は、そうである必要はありません。メソッドのスウィズリングについての考え方を少し変更するだけです。ほとんどのスウィズリングは次のように行われます:
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
これは問題なく機能しますが、my_setFrame:
別の場所で定義された場合はどうなりますか?この問題はスウィズリングに固有のものではありませんが、とにかく回避できます。この回避策には、他の落とし穴にも対処できるという追加の利点があります。代わりに、次のことを行います。
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
これはObjective-Cに少し似ていますが(関数ポインターを使用しているため)、名前の競合を回避します。原則として、標準的なスウィズリングとまったく同じことを行います。これは、しばらくの間定義されているため、スウィズリングを使用している人にとっては少し変更になるかもしれませんが、結局のところ、それはより良いと思います。スウィズリングメソッドはこのように定義されています。
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
メソッド名を変更してスウィズルすると、メソッドの引数が変更されます
これは私の心の中で大きな問題です。これが、標準的なメソッドのスウィズリングを行わない理由です。元のメソッドの実装に渡された引数を変更しています。これが起こる場所です:
[self my_setFrame:frame];
この行は次のとおりです。
objc_msgSend(self, @selector(my_setFrame:), frame);
ランタイムを使用しての実装を検索しますmy_setFrame:
。実装が見つかると、指定されたのと同じ引数で実装を呼び出します。見つかった実装はの元の実装でsetFrame:
あるため、先に進んでそれを呼び出しますが、_cmd
引数はそうsetFrame:
あるべきではありません。今my_setFrame:
です。元の実装は、それが受け取ることを予期していない引数で呼び出されています。これはダメです。
簡単な解決策があります。上記で定義した代替のスウィズリングテクニックを使用します。引数は変更されません。
スウィズルの順序は重要です
メソッドがスウィズルされる順序は重要です。setFrame:
でのみ定義されていると仮定してNSView
、次の順序を想像してください。
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
上のメソッドNSButton
がスウィズルされるとどうなりますか?ほとんどのスウィズリングはsetFrame:
、すべてのビューの実装を置き換えないことを保証するため、インスタンスメソッドをプルアップします。これは既存の実装を使用setFrame:
してNSButton
クラスを再定義するため、実装の交換がすべてのビューに影響を与えることはありません。既存の実装は、で定義されたものNSView
です。スウィズルしたときも同じことが起こりますNSControl
(ここでもNSView
実装を使用しています)。
あなたが呼び出すとsetFrame:
、ボタンの上に、それゆえ、あなたのスウィズルメソッドを呼び出して、その後に直接ジャンプしますsetFrame:
元々に定義された方法NSView
。スウィズル実装が呼び出されません。NSControl
NSView
しかし、注文が次の場合はどうなりますか?
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
ビューのスウィズリングが最初に行われるため、コントロールのスウィズリングは適切なメソッドを引き上げることができます。同様に、コントロールのスウィズリングはボタンのスウィズリングの前だったため、ボタンはコントロールのスウィズルされたの実装を引き上げますsetFrame:
。これは少し混乱しますが、これは正しい順序です。どのようにしてこの順序を保証できますか?
繰り返しますが、を使用load
して物事をスウィズルします。スウィズルしてload
、ロードされるクラスに変更を加えるだけであれば、安全です。このload
メソッドは、スーパークラスのloadメソッドがサブクラスの前に呼び出されることを保証します。正確な注文をお届けします!
理解しにくい(再帰的に見える)
伝統的に定義されたスウィズルメソッドを見ると、何が起こっているのかを理解するのは本当に難しいと思います。しかし、上記でスウィズルを行った別の方法を見ると、理解するのは非常に簡単です。これはすでに解決されています!
デバッグが難しい
デバッグ中の混乱の1つは、スウィズルされた名前が混同され、すべてが混乱する奇妙なバックトレースが表示されることです。ここでも、代替の実装がこれに対処します。バックトレースに明確に名前が付けられた関数が表示されます。それでも、スウィズリングの影響を思い出すことが難しいため、スウィズリングはデバッグが難しい場合があります。コードを適切に文書化します(たとえ自分だけがコードを見ると思うとしても)。良い習慣に従ってください、そうすれば大丈夫です。マルチスレッドコードよりもデバッグは難しくありません。
結論
メソッドのスウィズリングは、適切に使用すれば安全です。あなたが取ることができる簡単な安全対策は、でのみスウィズルすることload
です。プログラミングにおける多くのことのように、それは危険な場合がありますが、結果を理解することでそれを適切に使用することができます。
1上記で定義したスウィズリングメソッドを使用すると、トランポリンを使用する場合、スレッドセーフにすることができます。2つのトランポリンが必要になります。メソッドの開始時に、指すstore
先のアドレスstore
が変更されるまでスピンする関数に、関数ポインターを割り当てる必要があります。これにより、store
関数ポインターを設定できるようになる前に、スウィズルメソッドが呼び出される競合状態が回避されます。次に、実装がクラスでまだ定義されていない場合にトランポリンを使用し、トランポリンをルックアップしてスーパークラスメソッドを適切に呼び出す必要があります。スーパー実装を動的にルックアップするようにメソッドを定義すると、スウィズルコールの順序が重要ではなくなります。