Objective-CのidをSwiftのAny型としてimportする
Import Objective-C id as Swift Any type
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
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 制約が暗黙にオーバーロード解決や暗黙変換を誘導していた場面では、挙動が微妙に変わる可能性があります。
03 今後の見通し
id を Any にブリッジする普遍的な仕組みが整うことで、値型と Objective-C ブリッジの間に残っている表現上のギャップを、さらに解消していく方向性が示されています。いずれも将来的な構想であり、別 Proposal で個別に検討されるべきもので、実現を約束するものではありません。
Objective-C ジェネリクスの制約緩和
Objective-C のジェネリクスを Swift にインポートする際、型パラメータには AnyObject 制約が付いています。Any ベースのブリッジが整えば、この制約を外して、Objective-C のジェネリクスの型パラメータにも Swift の値型を使えるようにする方向が考えられます。
値型を Objective-C プロトコルへ適合させる
任意の Swift の値を Objective-C オブジェクトへブリッジできるなら、値型に対しても @objc プロトコルへの適合を許す道が開けます。ブリッジ先の Objective-C クラスが対応するメッセージに応答するようにすれば、Foundation などが提供するプロトコルを、Darwin と corelibs の双方で値型のまま扱えるようになります。
ただし、実現には次のようなトレードオフがあります。
- プロトコルに適合する値型ごとに、専用のボックス用 Objective-C クラスを生成する必要がある可能性があります。
StringとNSStringのように独自のブリッジを持つ型では、@objc適合をブリッジ先のクラスにも自動で反映させるかが論点になります。- delegate プロトコルのように、本来クラス制約を前提としているもの(
weak参照されるなど)への配慮が必要です。@objcがクラス制約を含意しなくなると、@objcプロトコル型のプロパティをweakにできなくなる可能性があります。
AnyObject のメソッドルックアップの扱いを見直す
現在の AnyObject には、任意の @objc メソッドを動的にルックアップできるという特別な振る舞いが備わっています。id が Any にブリッジされるようになると、この機能をどう扱うかを改めて考える必要があります。挙げられている選択肢は次のとおりです。
- 既存の振る舞いをそのまま
Anyに移す。 - 言語機能としては廃止し、必要であれば演算子と未適用メソッド参照を組み合わせて近い表現を実現する。
- プロパティと subscript のルックアップに範囲を限定する。
- SDK の Swift 化が進んで
id自体が稀になる前提で、置き換えを用意しない。
NSObjectProtocol を Swift から隠す
NSObjectProtocol への適合要件は、現実には Swift のクラスや Cocoa のクラスのほとんどが満たしているにもかかわらず、@objc 由来の冗長な要求として Swift コードに現れがちです。値型をブリッジするボックス用クラスにも NSObjectProtocol の機能を持たせ、Swift 側からはこの要求を見せないようにすることで、ネイティブな Swift クラスや値型をより滑らかに Cocoa と相互運用できるようになります。
より多くの型を慣用的な Objective-C オブジェクトへブリッジする
Any から id への変換を多くの型で適切に動作させることが重要になるため、ブリッジ対象を広げる方向も挙げられています。具体的には、
IntやDoubleだけでなく、すべての[U]IntNN/FloatNN型や Foundation のDecimalをNSNumberにブリッジする。CGRectやNSRangeのような Foundation / CoreGraphics の構造体をNSValueにブリッジする。Optionalを非 nullable なidに渡したときに、nilをNSNullへブリッジする。これにより[Foo?]のようなコレクションもNSArrayへ自然にブリッジできる。
といった候補があります。
純粋な Swift コードでの動的キャストの単純化
本 Proposal は、Objective-C から返ってくる id を Swift で扱うために ambivalent dynamic casting に依拠しています。一方で SE-0083 はこの振る舞いを Swift から取り除くことを目指していました。両者の折衷として、ambivalent dynamic casting を Objective-C 由来の Any に限定し、純粋な Swift コードではより予測しやすい単純なキャスト挙動にする、という方向もありえます。Swift 3 ではすべての existential が ambivalent な振る舞いをしますが、将来的にはランタイム上のフラグで条件付きに切り替えられる可能性があります。