動的メソッド呼び出しとは、コンパイル時ではなく、実行時に名前でメソッドを呼び出すことができる機能です。Swiftでは、@objcおよびdynamic修飾子を使用することによって、またはAnyObjectプロトコルを用いることによって、Objective-Cランタイムと連携することでこれが可能です。
メカニズム:
@objc — メソッド/プロパティ/クラスをObjective-Cランタイムにエクスポートするためにマークします。dynamic — スタティック呼び出し(vtableを通じた呼び出し)ではなく、Objective-Cメッセージディスパッチを介して実行されることを要求します。制約:
@objcでマークされたプロトコルでのみ機能します。@objc属性を持つメソッド/プロパティや、NSObjectからの継承を通じてマークされたメソッド/プロパティのみに適用されます。例:
import Foundation class Person: NSObject { @objc dynamic func sayHello() -> String { return "Hello!" } } let person = Person() // Selector (perform:)による動的呼び出し: if person.responds(to: #selector(Person.sayHello)) { if let result = person.perform(#selector(Person.sayHello))?.takeUnretainedValue() as? String { print(result) // "Hello!" } }
同様に、AnyObjectを使用してオプショナルチェイニングを介して関数を呼び出すこともできます:
@objc protocol Greeter { @objc optional func greet() } class Robot: NSObject, Greeter { func greet() { print("Beep-beep!") } } let anyGreeter: AnyObject = Robot() anyGreeter.greet?() // "Beep-beep!"
structは@objc、dynamic、またはAnyObjectを介して動的メソッド呼び出しをサポートできますか?
回答: いいえ、NSObjectから継承されたクラス、または@objc修飾子を持つクラス/プロトコルのみがそのようなメカニズムをサポートできます。StructやenumはObjective-Cランタイムと互換性がないため、動的なメンバーにはなれません。
物語
プロジェクトで@objcプロトコルをデリゲートとして使用していました。開発者の一人がデリゲートクラスのNSObjectからの継承を取り除いたため、オプショナルメソッドがオプショナルチェイニングを介して呼び出せなくなりました。アーキテクチャが機能しなくなり、テストが常に失敗するようになりました。
物語
構造体のプロパティにKVO(key-value observing)を使用しようとする際に、開発者はそれをdynamicとしてマークしました。コードはコンパイルされましたが、期待通りに動作しませんでした。なぜなら、structはObjective-Cランタイムをサポートしていなかったからです。変更に対する動的な反応が機能せず、UIの更新が見逃されました。
物語
クラスの拡張でperform(_:で呼び出される関数に@objc修飾子を追加するのを忘れました。その結果、実行時に無関係なセレクタを呼び出そうとしたときにクラッシュが発生しました。原因を長い間探しましたが、関数がObjective-Cにエクスポートされていないことに気づくまでかかりました。