239

メソッドの入れ替えは危険な行為だと人々が言うのを聞いたことがあります。スウィズリングという名前でさえ、それが少しチートであることを示唆しています。

メソッド スウィズリングは、呼び出しセレクター A が実際に実装 B を呼び出すようにマッピングを変更しています。これの 1 つの用途は、クローズド ソース クラスの動作を拡張することです。

スウィズリングを使用するかどうかを決定しているすべての人が、自分がやろうとしていることに価値があるかどうかを十分な情報に基づいて決定できるように、リスクを形式化できますか。

例えば

  • 名前の衝突: クラスが後でその機能を拡張して、追加したメソッド名を含めると、大きな問題が発生します。慎重に入れ替わったメソッドに名前を付けることで、リスクを軽減します。
4

8 に答える 8

447

これは本当に素晴らしい質問だと思います。実際の質問に取り組むのではなく、ほとんどの回答が問題を回避し、単にスウィズリングを使用しないと述べているのは残念です.

メソッド シズリングを使用することは、キッチンで鋭いナイフを使用するようなものです。よく切れる包丁を怖がる方もいますが、切れ味の悪い包丁の方が安全です。

メソッド スウィズリングを使用すると、より優れた、より効率的で保守しやすいコードを記述できます。また、悪用されて恐ろしいバグにつながる可能性もあります。

バックグラウンド

すべての設計パターンと同様に、パターンの結果を十分に認識していれば、それを使用するかどうかについて、より多くの情報に基づいた決定を下すことができます。シングルトンはかなり物議をかもしているものの良い例であり、それには正当な理由があります — シングルトンを適切に実装するのは非常に困難です。ただし、多くの人はまだシングルトンを使用することを選択しています。スウィズリングについても同じことが言えます。良い面と悪い面の両方を完全に理解したら、自分の意見を形成する必要があります。

討論

メソッド スウィズリングの落とし穴のいくつかを次に示します。

  • メソッドの入れ替えはアトミックではない
  • 所有されていないコードの動作を変更します
  • 命名の競合の可能性
  • スウィズリングはメソッドの引数を変更します
  • スウィズルの順序が重要
  • わかりにくい(再帰的に見える)
  • デバッグが難しい

これらのポイントはすべて有効であり、それらに対処することで、メソッドの入れ替えと結果を達成するために使用される方法論の両方の理解を深めることができます. 一つ一つ取っていきます。

メソッドの入れ替えはアトミックではない

同時に使用しても安全なメソッド スウィズリングの実装はまだ見たことがありません1。これは実際には、メソッド スウィズリングを使用する 95% のケースでは問題になりません。通常、メソッドの実装を単に置き換えたいだけであり、その実装をプログラムの存続期間全体にわたって使用したいと考えています。これは、 でメソッドのスウィズリングを行う必要があることを意味します+(void)loadloadクラス メソッドは、アプリケーションの開始時に順次実行されます。ここでスウィズルを実行しても、同時実行性に問題はありません。ただし、をスウィズルする+(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:)];

メソッド onNSButtonがスウィズルされるとどうなりますか? ほとんどのスウィズルはsetFrame:、すべてのビューの実装を置き換えないことを保証するため、インスタンス メソッドをプルアップします。setFrame:これにより、既存の実装を使用してクラスで再定義されるNSButtonため、実装の交換がすべてのビューに影響することはありません。既存の実装は で定義されたものNSViewです。NSControl(再びNSView実装を使用して)スウィズルをオンにすると、同じことが起こります。

したがって、ボタンを呼び出すsetFrame:と、swizzled メソッドが呼び出され、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を設定できるようになる前に、swizzled メソッドが呼び出されるという競合状態が回避されます。store実装がクラスでまだ定義されていない場合はトランポリンを使用し、トランポリンを検索してスーパークラスメソッドを適切に呼び出す必要があります。スーパー実装を動的に検索するようにメソッドを定義すると、スウィズリング呼び出しの順序が問題にならないことが保証されます。

于 2011-12-26T14:15:19.833 に答える
12

最初に、メソッドの入れ替えの意味を正確に定義します。

  • 最初にメソッド (A と呼ばれる) に送信されたすべての呼び出しを新しいメソッド (B と呼ばれる) に再ルーティングします。
  • 私たちはメソッドBを所有しています
  • 私たちはメソッドAを所有していません
  • メソッド B は何らかの処理を行ってから、メソッド A を呼び出します。

メソッドの入れ替えはこれよりも一般的ですが、これは私が興味を持っているケースです。

危険:

  • 元のクラスの変更。スウィズルしているクラスを所有していません。クラスが変わると、スウィズルが機能しなくなる可能性があります。

  • 維持するのが難しい。スウィズルされたメソッドを作成して保守する必要があるだけではありません。スウィズルを実行するコードを作成して維持する必要があります

  • デバッグが難しい。スウィズルの流れを追うのは難しく、スウィズルが実行されたことに気付かない人もいます。スウィズルから導入されたバグ (おそらく元のクラスの変更によるもの) がある場合、それらを解決するのは困難です。

要約すると、スウィズルを最小限に抑え、元のクラスの変更がスウィズルにどのように影響するかを検討する必要があります。また、自分が行っていることを明確にコメントして文書化する必要があります (または、完全に回避する必要があります)。

于 2011-03-18T20:38:29.037 に答える
7

本当に危険なのはスウィズリングそのものではありません。問題は、あなたが言うように、フレームワーク クラスの動作を変更するためによく使用されることです。これらのプライベート クラスがどのように機能するかについて「危険」なことを知っていることを前提としています。あなたの変更が現在機能していても、Apple が将来クラスを変更し、あなたの変更が壊れる可能性は常にあります。また、多くの異なるアプリがそれを行う場合、Apple が多くの既存のソフトウェアを壊すことなくフレームワークを変更することは非常に難しくなります。

于 2011-03-17T13:16:06.337 に答える
5

私はこの手法を使用しましたが、次のことを指摘したいと思います。

  • 文書化されていない、望ましい副作用を引き起こす可能性があるため、コードが難読化されます。コードを読んでも、コードベースを検索してスウィズルされているかどうかを確認することを覚えていない限り、必要な副作用の動作に気付かない可能性があります。この問題を軽減する方法がわかりません。コードが副作用のスウィズル動作に依存しているすべての場所を常に文書化できるとは限らないためです。
  • 他の場所で使用したいスウィズル動作に依存するコードのセグメントを見つけた人は、スウィズルされたメソッドを見つけてコピーせずに他のコードベースに単純にカットアンドペーストできないため、コードの再利用性が低下する可能性があります。
于 2014-02-09T03:24:30.880 に答える
5

慎重かつ賢明に使用すると、洗練されたコードにつながる可能性がありますが、通常は混乱を招くコードにつながるだけです。

特定の設計タスクに非常にエレガントな機会を提供することをたまたま知っていない限り、禁止すべきだと言いますが、なぜそれが状況にうまく適用されるのか、そしてなぜ代替案が状況に対してエレガントに機能しないのかを明確に知る必要があります.

たとえば、メソッド スウィズリングの優れたアプリケーションの 1 つはスウィズリングです。これは、ObjC が Key Value Observing を実装する方法です。

悪い例としては、クラスを拡張する手段としてメソッド スウィズリングに依存している可能性があります。これは非常に高い結合につながります。

于 2011-03-17T13:16:06.637 に答える
4

最大の危険は、完全に偶然に多くの望ましくない副作用を生み出すことだと思います. これらの副作用は「バグ」として現れ、解決策を見つけるために間違った道をたどってしまう可能性があります。私の経験では、コードが判読できず、混乱し、イライラさせられる危険性があります。誰かが C++ で関数ポインタを使いすぎたときのようなものです。

于 2011-03-17T13:10:19.420 に答える
3

メソッドの入れ替えは、単体テストで非常に役立ちます。

モック オブジェクトを作成し、そのモック オブジェクトを実際のオブジェクトの代わりに使用することができます。コードはクリーンな状態を保ち、単体テストの動作は予測可能です。CLLocationManager を使用するコードをテストしたいとしましょう。単体テストは startUpdatingLocation を切り替えて、事前に定義された場所のセットをデリゲートにフィードし、コードを変更する必要がないようにすることができます。

于 2014-08-09T14:34:36.927 に答える