SE-160 Limiting @objc inferenceの概要
TL;DR
- Objective-Cランタイムから呼ぶ必要があるものは明示的に
@objc
の付与が必要になった(今まではコンパイラが勝手につけていた) -
@objc
をクラス全体に適用する@objcMembers
が追加になった - いらない内部コードが減ってバイナリサイズが6-8%減る
- マイグレーションとかはXcodeのマイグレータでほぼOK
前提
Swift3では@objc
推論が多く行われていた
(@objc
とはSwiftのコードをObjective-Cランタイムから呼べるようにするためのもの)
以下の場合に暗黙的に@objc
推論が行われている
- Objective-Cのクラスを継承している
- dynamicキーワードを付与している変数やメソッド
- etc.
提案された理由
-
@objc
推論される規則がわかりにくい - Objective-Cのセレクターの衝突が起こりがち
class MyNumber : NSObject {
init(_ int: Int) { }
init(_ double: Double) { } // error: initializer 'init' with Objective-C selector 'init:'
// conflicts with previous declaration with the same Objective-C selector
}
- Swift API Design Guidelinesに沿って書いても、Objective-Cから呼び出す時にObjective-C Coding Guidelines for Cocoaに反する名前に変化されがち
- 特にinitializerの中では第一引数名を含めないといけないので
@objc
を合わせて書かなくてはいけなくなる
class MyNumber : NSObject {
@objc(initWithInteger:) init(_ int: Int) { }
@objc(initWithDouble:) init(_ double: Double) { }
}
- SwiftコンパイラはObjective-C Calling ConventionからSwift Calling Conventionを関連づけるthunkメソッドをObjective-Cメタデータ内に記録するためバイナリサイズが大きくなってしまう
- Objective-C entry pointごとにコストがかかる
- thunkメソッドにより6-8%のバイナリサイズが大きくなる(未使用のものも含まれている)
Apple's Music Appでは5.7%サイズが減った
提案
プログラムの整合性上、必要な部分を除き@objc
推論をしないようにする
その上で広範囲で@objc
をon/offする時に定型記述を減らすためにclassレベル、extensionレベルのアノテーションを追加する
引き続き推論されるパターン
-
@objc
属性がついているものをオーバーライドする場合 -
Super.foo()
に対するObjective-C callersがオーバーライドしたSub.foo()
を適切に呼び出すため
class Super {
@objc func foo() { }
}
class Sub : Super {
/* inferred @objc */
override func foo() { }
}
-
@objc
属性がついているprotocol要件を満たす時 -
MyDelegate.bar()
がObjective-C message sendを介して呼ばれるため
@objc protocol MyDelegate {
func bar()
}
class MyClass : MyDelegate {
/* inferred @objc */
func bar() { }
}
-
@IBAction
,@IBOutlet
属性がついている時 - Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため
-
@NSManaged
属性がついている時 - Core DataとのインタラクションがObjective-Cランタイムで行われるため
追加で推論されるパターン
-
@GKInspectable
属性がついている時 - GameplayKitとのインタラクションがObjective-Cランタイムで行われるため
-
@IBInspectable
属性がついている時 - Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため
推論されなくなるパターン
- dynamicキーワードが付与されているもの
- dynamicは動的ディスパッチをするためのもの(Objective-Cランタイムからよびだされる)なのでSwift3の時は
@objc
が推論されていた
class MyClass {
dynamic func foo() { } // error: 'dynamic' method must be '@objc'
@objc dynamic func bar() { } // okay
}
- NSObject由来のもの
- これがこの提案で唯一の大きな変更
- これにより多くのメソッドをObjective-Cから呼べなくなる(これによりバイナリサイズが小さくなる)
class MyClass : NSObject {
func foo() { } // not exposed to Objective-C in Swift 4
}
Swift3時代はObjective-Cで表現できるもののみ@objc
推論されていた
extension MyClass {
func bar(param: ObjCClass) { } // Swift3では推論された。この提案では推論されない
func baz(param: SwiftStruct) { } // StructはObjective-Cでは表現できないので推論されない
}
クラス全体で@objc
推論を有効にさせる(@objcMembers
)
XCTest,Realmのようないくつかのライブラリは依然としてObjective-Cランタイムに依存する
そのためクラス、そのExtension、サブクラス、およびそれらすべてのExtensionに対して今まで通りの@objc
推論を一括で行うため
@objcMembers
属性を新しく追加する。
@objcMembers
class MyClass : NSObject {
func foo() { } // implicitly @objc
func bar() -> (Int, Int) // not @objc, because tuple returns
// aren't representable in Objective-C
}
extension MyClass {
func baz() { } // implicitly @objc
}
class MySubClass : MyClass {
func wibble() { } // implicitly @objc
}
extension MySubClass {
func wobble() { } // implicitly @objc
}
また以下のようにswift_objc_members
を付与することで
インポートされたObjective-Cクラスを@objcMembers
としてSwiftにインポートすることができる
__attribute__((swift_objc_members))
@interface XCTestCase : XCTest
/* ... */
@end
↓
@objcMembers
class XCTestCase : XCTest { /* ... */ }
Extensionでの@objc
のon/off
Extensionに@objc
or @nonobjc
属性をつけることで
そのExtension内の@objc
or@nonobjc
属性を個別で持ってないメンバーに対して一律適用できる
class SwiftClass { }
@objc extension SwiftClass {
func foo() { } // implicitly @objc
func bar() -> (Int, Int) // error: tuple type (Int, Int) not
// expressible in @objc. add @nonobjc or move this method to fix the issue
}
@objcMembers
class MyClass : NSObject {
func wibble() { } // implicitly @objc
}
@nonobjc extension MyClass {
func wobble() { } // not @objc, despite @objcMembers
}
Swift4へのマイグレーション
Step1:
Swift4モードで、@objc
推論されている警告の対処をする
Step2:
環境変数SWIFT_DEBUG_IMPLICIT_OBJC_ENTRYPOINT
の値を1〜3にしてアプリをテストする
- ObjectiveCのエントリポイントへの呼び出しを記録
ex.***Swift runtime: entrypoint -[MyApp.MyClass foo] generated by implicit @objc inference is deprecated and will be removed in Swift 4
- 1 + バックトレースを発行
- 2 + クラッシュ
Step3:
この時点でSwift4へマイグレーション可能になる
Swift4でビルドすると、廃止予定のルールに基づき@objc
推論されたObjective-Cエントリポイントが削除される
※実際はXcodeのマイグレーターがよしなにやってくれるので、自分のプロジェクトではここら辺は何も考える必要がなかった