Import Objective-C id as Swift Any type
01 何が問題だったのか
Objective-C の id 型は、Swift のこれまでの版では AnyObject としてインポートされていました。これは直感的な対応ですが、Swift と Objective-C の間で大きな摩擦を生んでいました。
Swift の特徴のひとつは String / Array / Dictionary といった値型が中心にあることで、これらはクラスの共有による予期しない状態の変化を避けつつ、効率的な書き換えを可能にします。一方で Objective-C の多態的なインターフェースは id を多用しており、それらは Swift に AnyObject として渡ってきます。値型と AnyObject はそのままでは噛み合わないため、これまでは次のような特別な言語機能で辻褄を合わせていました。
- ブリッジ可能な値型(
StringとNSStringなど)は、Objective-C のクラス型へ 暗黙に変換 される。これにより、たとえば[AnyObject]として異種の値を集めてNSArrayに渡すといった使い方ができた。 - 静的型が
AnyObjectの値を、is/as?/as!によって元の Swift の値型に動的にキャストできる(ambivalent dynamic casting)。
しかし、これらは言語の他の部分と一貫しない特別扱いであり、予期しない振る舞いの温床になっていました。そのため、暗黙のブリッジ変換を廃止する SE-0072 などが別途検討されています。
さらに、Swift 3 の Foundation は広範囲に値型を採用しており、問題の範囲は標準ライブラリ内の一部の型だけに収まらなくなっています。加えて Swift と Foundation は Objective-C ランタイムのない環境(非 Apple プラットフォーム)にも移植されつつあり、そのような環境でも同じ Foundation API を提供しようとすると、AnyObject を前提とした API は不自然になります。
根本的な緊張は、Objective-C の多態性がオブジェクト(クラス)を中心にしているのに対し、Swift の多態性はすべての型に開かれていることにあります。id を AnyObject に対応させているかぎり、この緊張は解消できません。
02 どのように解決されるのか
Objective-C の id を、Swift 側では AnyObject ではなく Any としてインポートするようにします。これに伴い、id を境に値型と Objective-C オブジェクトを行き来するための仕組みを、特別な言語機能ではなくコンパイラとランタイムの内部機構として整備します。
Swift から Objective-C へ: 普遍的なブリッジ変換
Swift から Objective-C へ Any の値を id として渡すとき、コンパイラとランタイムがあらゆる Swift の値を Objective-C オブジェクトへ橋渡しする universal bridging conversion を行います。扱いは次の3種類に分かれます。
- クラス: クラス参照はそのまま Objective-C のオブジェクト参照として渡されます。
- ブリッジ可能な値型:
String/Array/Dictionary/Setなど、従来からブリッジ対応している値型は、これまでと同様に対応する Cocoa クラス(NSString/NSArrayなど)のインスタンスへ変換されます。どの型がブリッジ可能かは内部プロトコル_ObjectiveCBridgeableへの適合で決まり、将来的に拡張されていきます。 - ブリッジ対応のない値型: これまで
idとして渡せなかったような独自のstructやenumについても、コンパイラが暗黙のボックス用クラスでくるんでidとして渡します。Objective-C 側ではただの不透明なオブジェクトに見えますが、Swift へ戻ってきたときには元の値型として取り出せます。
この結果、すべての Swift の値を id として受け渡せるようになり、値型と Objective-C API の相互運用が自然になります。
Objective-C から Swift へ: Any からの動的キャスト
Objective-C から id として返ってきた値は、Swift 側では Any として受け取ります。そこから具体的な型を取り出すには、従来どおり is / as? / as! による動的キャストを使います。
ランタイムの ambivalent dynamic casting という仕組みにより、Any に入っている値は 元のクラスとしても、対応する Swift の値型としても キャストが成功します。これは、Objective-C から返ってきた値が Swift 側で値型として使われるか、そのままクラス参照として使われるか、受け取り時には判断できないためです。
var x: Any = "foo" as String
x as? String // => String "foo"
x as? NSString // => NSString "foo"
x = "bar" as NSString
x as? String // => String "bar"
x as? NSString // => NSString "bar"
Objective-C のコレクションのインポート
単一の id と同じ考え方をコレクションにも適用し、型付けのない Cocoa コレクションを次のように Swift 側へインポートします。
NSArray→[Any]NSDictionary→[AnyHashable: Any]NSSet→Set<AnyHashable>
[T] を [Any] のサブタイプとして扱えるよう、配列の共変変換からはこれまでのクラス制約が外されます。
Dictionary や Set のキーは Hashable である必要があるため、異種の Hashable な値をまとめて保持できる型消去コンテナとして AnyHashable が新たに導入されます(詳細な設計は別 Proposal で扱われます)。Hashable のプロトコル型そのものを使えないのは、Hashable が Self 制約を要求する Equatable を継承しており、プロトコル型自身がそのプロトコルに適合しないという Swift 3 時点の制約があるためです。
既存コードへの影響
SE-0072 による暗黙のブリッジ変換の撤廃と組み合わせることで、多くの Swift 2 コードは大きな書き換えなしに動作を維持できます。値型をそのまま Objective-C API に渡せるようになるため、明示的なブリッジ・逆ブリッジの記述も多くの場面で不要になります。ただし、これまで AnyObject 制約が暗黙にオーバーロード解決や暗黙変換を誘導していた場面では、挙動が微妙に変わる可能性があります。
今後の見通し
id を Any にブリッジする基盤が整うことで、将来的には次のような方向への拡張が見込まれます(これらは別 Proposal で個別に検討されるものであり、実現を約束するものではありません)。
- Objective-C のジェネリクスの型パラメータから
AnyObject制約を外し、値型でも使えるようにする。 - 値型が Objective-C プロトコルに適合できるようにする。
- 現在
AnyObject型に備わっている「任意の@objcメソッドを動的にルックアップできる」機能の扱いを見直す(Anyに移す、範囲を縮める、廃止するなどの選択肢が議論されています)。 Int/Double以外の整数・浮動小数点型やDecimalをNSNumberに、CGRectやNSRangeをNSValueにブリッジするなど、より多くの型を慣用的な Objective-C オブジェクトへ橋渡しする。- Objective-C 相互運用のない純粋な Swift コードでは ambivalent dynamic casting を限定し、キャストの挙動をより予測しやすくする。