Objective-Cでのメソッドのスウィズリングの危険性は何ですか?


235

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

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

リスクを形式化して、スウィズリングを使用するかどうかを決定する誰もが、彼らがやろうとしていることに対してそれが価値があるかどうか、情報に基づいた決定をすることができるようにできますか?

例えば

  • 衝突の命名:クラスが後でその機能を拡張して、追加したメソッド名を含めると、巨大な方法で問題が発生します。スウィズルされたメソッドに賢明な名前を付けることにより、リスクを軽減します。

それはあなたが読んだコードから期待して何か他のものを得るためにまだ非常に良いではありません
マーティンBabacaev

これは、Pythonデコレータが非常にクリーンに行うことを行うための不潔な方法のように聞こえます
Claudiu

回答:


439

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

焼けるように暑い方法を使用すると、キッチンで鋭いナイフを使用するようなものです。一部の人々は彼らが自分自身をひどく切るだろうと思うので鋭利なナイフを怖がっていますが、真実は鋭利なナイフがより安全であるということです。

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

バックグラウンド

すべてのデザインパターンと同様に、パターンの結果を十分に認識している場合は、パターンを使用するかどうかについて、より多くの情報に基づいた決定を行うことができます。シングルトンはかなり物議を醸す何かの良い例であり、正当な理由で—シングルトンは適切に実装するのが本当に難しいです。ただし、多くの人はまだシングルトンを使用することを選択しています。スウィズリングについても同じことが言えます。良い点と悪い点の両方を十分に理解したら、あなた自身の意見を形成するべきです。

討論

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

  • メソッドのスウィズリングはアトミックではありません
  • 所有されていないコードの動作を変更します
  • 考えられる名前の競合
  • スウィズリングはメソッドの引数を変更します
  • スウィズルの順序は重要です
  • 理解しにくい(再帰的に見える)
  • デバッグが難しい

これらの点はすべて有効であり、それらに対処することで、メソッドのスウィズリングの理解と結果を達成するために使用される方法の両方を改善できます。一度に1つずつ取ります。

メソッドのスウィズリングはアトミックではありません

同時に使用しても安全なメソッドswizzlingの実装はまだ見ていません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:)];

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

あなたが呼び出すとsetFrame:、ボタンの上に、それゆえ、あなたのスウィズルメソッドを呼び出して、その後に直接ジャンプしますsetFrame:元々に定義された方法NSView。スウィズル実装が呼び出されません。NSControlNSView

しかし、注文が次の場合はどうなりますか?

[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関数ポインターを設定できるようになる前に、スウィズルメソッドが呼び出される競合状態が回避されます。次に、実装がクラスでまだ定義されていない場合にトランポリンを使用し、トランポリンをルックアップしてスーパークラスメソッドを適切に呼び出す必要があります。スーパー実装を動的にルックアップするようにメソッドを定義すると、スウィズルコールの順序が重要ではなくなります。


24
驚くほど有益で技術的に健全な答え。議論は本当に面白いです。これを書いてくれてありがとう。
Ricardo Sanchez-Saez 2013年

あなたの体験のスウィズルがアプリストアで受け入れられるかどうか指摘していただけませんか。stackoverflow.com/questions/8834294にも案内します。
Dickey Singh

3
スウィズリングはApp Storeで使用できます。多くのアプリとフレームワークが対応しています(当社の料金が含まれています)。上記のすべてがまだ保持されており、プライベートメソッドをスウィズルすることはできません(実際には、技術的にはできますが、拒否のリスクがあり、これの危険性は別のスレッドにあります)。
wbyoung 2013年

すばらしい答え。しかし、なぜ+ swizzle:with:store:で直接ではなく、class_swizzleMethodAndStore()でスウィズリングを行うのですか?なぜこの追加機能なのですか?
Frizlab、2014

2
@Frizlab良い質問!それは本当にただのスタイルのものです。Objective-CランタイムAPI(C)で直接動作する一連のコードを記述している場合、一貫性を保つためにCスタイルと呼ばれると便利です。これ以外に私が考えることができる唯一の理由は、純粋なCで何かを書いた場合、それでも呼び出し可能であるということです。ただし、Objective-Cメソッドですべてを実行できないわけではありません。
wbyoung '25

12

最初に、メソッドのスウィズリングによって私が意味することを正確に定義します。

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

メソッドのスウィズリングはこれよりも一般的ですが、私が興味を持っているのはこれです。

危険:

  • 元のクラスの変更。スウィズルしているクラスは所有していません。クラスが変更されると、スウィズルが機能しなくなる場合があります。

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

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

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


@誰もがあなたの答えを素晴らしいフォーマットにまとめました。自由に編集/追加してください。ご入力いただきありがとうございます。
ロバート

私はあなたの心理学のファンです。「大きなスウィズリングパワーには、大きなスウィズリングの責任が伴います。」
Jacksonkr 2017

7

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


私はそのような開発者が彼らがまくものを刈り取るべきだと思います-彼らは彼らのコードを特定の実装にしっかりと結び付けました。そのため、実装が微妙に変化し、コードを破壊するべきではないという明確な決定がなければ、コードを壊さないようにするのが彼らの責任です。
アラファンギオン2011年

もちろん、非常に人気のあるアプリがiOSの次のバージョンで機能しなくなると言います。それは開発者のせいだと言うのは簡単であり、彼らはよりよく知る必要がありますが、それはAppleをユーザーの目に悪い見栄えにします。コードをやや混乱させる可能性があるという点を除けば、独自のメソッドまたはクラスをスウィズルすることに害を及ぼすことはないと思います。他の誰かによって制御されているコードをスウィズルすると、少し危険になり始めます。それが、スウィズリングが最も魅力的と思われる状況です。
カレブ

アップルは、swizzlingを介して変更できる基本クラスの唯一のプロバイダーではありません。回答を編集して、全員を含める必要があります。
AR

@AR多分、質問はiOSおよびiPhoneにタグ付けされているので、そのコンテキストでそれを考慮する必要があります。iOSアプリは、iOSが提供するもの以外のライブラリとフレームワークを静的にリンクする必要があるため、アプリ開発者の関与なしにフレームワーククラスの実装を変更できる唯一のエンティティはAppleです。しかし、同様の問題が他のプラットフォームにも影響を与えるという点には同意します。アドバイスは、スウィズルを超えて一般化することもできると思います。一般的な形式では、アドバイスは次のとおりです。他の人が保守するコンポーネントを変更しないでください。
カレブ

メソッドのスウィズリングは危険です。しかし、フレームワークが出てくると、それらは以前のものを完全に省略しません。それでも、アプリが期待する基本フレームワークを更新する必要があります。そしてその更新はあなたのアプリを壊すでしょう。iOSが更新されても、それほど大きな問題にはならない可能性があります。私の使用パターンは、デフォルトのreuseIdentifierをオーバーライドすることでした。私が_reuseIdentifier変数にアクセスできず、それが提供されない限りそれを置き換えたくなかったので、私の唯一の解決策は、すべてをサブクラス化して再利用識別子を提供するか、スウィズルしてnilの場合はオーバーライドすることでした。実装タイプが重要です...
Lazy Coder

5

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

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

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

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


1
-1(メソッドswizzlingのコンテキストでのKVOの言及)。isa swizzlingは別のことです。
Nikolai Ruhe 2013年

@NikolaiRuhe:私が啓発できるように、参考資料を提供してください。
Arafangion 2013年


5

私はこのテクニックを使用しましたが、それを指摘したいと思います:

  • ドキュメント化されていないものの、望ましい副作用が発生する可能性があるため、コードが難読化されます。コードを読み取るとき、コードベースを検索してスウィズルされたかどうかを覚えていない限り、必要な副作用の動作に気付かない可能性があります。この問題を緩和する方法がわからないのは、コードが副作用の変化に依存しているすべての場所を常に文書化できるとは限らないためです。
  • 他の場所で使用したいスウィズルされた動作に依存するコードのセグメントを見つけた人が、スウィズルされたメソッドを見つけてコピーしない限り、他のコードベースに単純にカットアンドペーストできないため、コードの再利用性が低下する可能性があります。

4

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


さて、問題はデバッグが難しいことです。この問題は、レビュー担当者がコードがスウィズルされたことを認識/忘れず、デバッグ時に間違った場所を探しているために発生します。
Robert

3
そうだね。また、使い過ぎると、無限のチェーンまたはスウィズルのウェブに入る可能性があります。そのうちの1つだけを将来的に変更するとどうなるか想像してみてください。ティーカップを積み重ねたり、「コードジェンガ」を演奏したりするのが好きです
AR

4

あなたは次のような奇妙に見えるコードになるかもしれません

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

いくつかのUIマジックに関連する実際の製品コードから。


3

ユニットテストでは、メソッドのスウィズリングが非常に役立ちます。

これにより、モックオブジェクトを記述し、実際のオブジェクトの代わりにそのモックオブジェクトを使用させることができます。クリーンな状態を維持するコードとユニットテストの動作は予測可能です。CLLocationManagerを使用するコードをテストするとします。ユニットテストはstartUpdatingLocationをスウィズルできるため、事前に定義された一連の場所をデリゲートに供給し、コードを変更する必要はありません。


isa-swizzlingの方が適しています。
vikingosegundo 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.