Remove bridging conversion behavior from dynamic casts
01 何が問題だったのか
Swift の動的キャスト as? / as! / is は、本来「値の動的な型をチェックして、合致していればその型として取り出す」ための機能です。ところがこれらの演算子には、純粋な型チェックに加えて Cocoa の橋渡し変換(bridging conversion) までこっそり組み込まれていました。たとえば Any に入った String を NSString にキャストすると、型チェックではなく「String から NSString への値変換」が走り、キャストが成功します。
// An Any that dynamically contains a value "foo": String
let x: Any = "foo"
// Cast succeeds and produces the bridged "foo": NSString
let y = x as! NSString
動的キャストに詰め込まれていた役割
現状の as? / as! / is は、おおまかに次の3系統の仕事を抱えていました。
- 純粋な型チェック
- クラスインスタンスが特定のクラスか確認する
- existential(
protocol型の値)が特定の型を包んでいるか確認する - ジェネリックな値を別の具体型として扱えるか確認する
- 値の型がプロトコルに適合しているか確認し、適合していれば existential に包み直す
- Cocoa の橋渡し変換
- 値が
_ObjectiveCBridgeableに適合していれば、対応する Cocoa クラスへ変換する(およびその逆) ErrorProtocol(現在のError)適合型をNSErrorへ橋渡しするNSErrorを、ドメインとコードが一致する Swift エラー型へ復元する(_ObjectiveCBridgeableErrorProtocol経由)
- 値が
- 言語の暗黙変換を動的に引き継ぐ挙動
Optionalをくぐり抜けて中身にキャストを試みるArray/Dictionary/Setの要素に対してキャストを波及させる(共変な要素変換)
このうち 2 は、他の言語機能とは性格がかなり異なります。型チェックではなく「値を別の表現に作り直す」動作だからです。
混ざっていることで何が困るのか
役割が混在していることで、次のような問題が生じていました。
- 挙動の驚き:
as?/as!は「型を確認するだけ」と思って書くと、Cocoa 型が絡んだ瞬間に値変換まで走ってしまう。どこで安価な型チェックが終わり、どこから重い橋渡しが始まるのかが読めません。 - パフォーマンス上の不透明さ: 動的キャストの中に橋渡し処理が隠れるため、
as?一つのコストが型の組み合わせで大きく変わります。 - 値型とクラスの特別扱い:
String↔NSStringのような Foundation の組だけがキャスト演算子に特別なショートカットを持ち、ユーザー定義型との扱いが非対称でした。 asの特例:asは本来「暗黙変換を明示的に書き下すための型注釈」でしたが、橋渡しのためだけにstring as NSStringのような「暗黙変換が存在しないのにasが通る」特別扱いが残っていました。as!とas?の非一貫: コレクション型の橋渡しではobject as! Array<T>が遅延(lazy)バージョン、object as? Array<T>が即時(eager)バージョンという食い違いがあり、一般的なx as! T≡(x as? T)!の関係が崩れる原因になっていました。catch let x as NSErrorの副作用: Swift のエラーをNSErrorとして扱うには動的キャストに頼るしかなく、エラーを扱う API 全体がこの橋渡しに依存していました。
要するに、as? / as! / is が「型チェック」と「橋渡し変換」という別種の操作を一つの構文に押し込んでいたことが、言語とランタイムの両面で複雑さの温床になっていた、というのがこの提案の問題意識です。
02 どのように解決されるのか
この提案は Rejected(却下) となりました。動的キャストから Cocoa 橋渡し変換を剥がすという方向性そのものは実装されず、現在の Swift でも as? / as! / is は String / NSString 間の橋渡しや、Error ↔ NSError の橋渡しを従来どおり担っています。
それでも、提案の要点を押さえておくと、「なぜ動的キャストに橋渡しが混ざっているのか」「将来また議論になりそうな論点は何か」を理解する助けになります。
提案されていた内容(採用されなかったもの)
Joe Groff による提案の核は次の3点でした。
as?/as!/isを 純粋な型チェック専用 に戻す。具体的には、先に挙げた役割のうち「Cocoa 橋渡し変換」「ErrorProtocol適合型とNSErrorの相互変換」を動的キャストから取り除く。as演算子も、橋渡し専用の特別扱い(string as NSStringなど「暗黙変換がないのにasが通る」形)をやめ、通常の型注釈としての役割だけに戻す。- 動的キャストから取り除いた機能は、型の通常のイニシャライザ として再提供する。
結果として、動的キャストは「今その値が指定の型として扱えるか」を判定するだけの、予測可能で軽い操作になるはずでした。
置き換え API として想定されていた形
橋渡しは値を保ったままの変換なので、標準ライブラリの慣習であるラベル無しの init(_:) で提供する、という方針が採られていました。
extension String {
init(_ ns: NSString) {
self = ._unconditionallyBridgeFromObjectiveC(ns)
}
}
extension NSString {
// factory init が使える前提のイメージ
factory init(_ string: String) {
self = string._bridgeToObjectiveC()
}
}
NSError については、ErrorProtocol 適合型から作るイニシャライザを Foundation オーバーレイが提供する想定です。
extension NSError {
factory init(_ error: ErrorProtocol) {
self = _bridgeErrorProtocolToNSError(error)
}
}
NSError を特定の Swift エラー型に復元する側は失敗可能イニシャライザで表現されます。
public protocol _ObjectiveCBridgeableErrorProtocol: ErrorProtocol {
init?(_ bridgedNSError: NSError)
}
Array / Dictionary / Set のように要素型の橋渡し可否で成否が決まるコンテナは、失敗可能イニシャライザを使います。
extension Array {
init?(_ ns: NSArray) {
var result: Array? = nil
if Array._conditionallyBridgeFromObjectiveC(ns, &result) {
self = result!
return
}
return nil
}
}
従来 object as! Array<T> が持っていた 遅延橋渡し(要素アクセス時に型を検査し、ミスマッチならトラップ)は、意図が分かるラベルを付けた別のイニシャライザとして切り出す案でした。
extension Array {
init(forcedLazyBridging object: NSArray) {
var result: Array? = nil
Array._forceBridgeFromObjectiveC(object, &result)
self = result!
}
}
こうしておけば、Array<T>(object)! は「即時に変換してそれに失敗したらトラップ」という一般的な x as! T ≡ (x as? T)! の関係と揃い、「遅延でよければ Array<T>(forcedLazyBridging: object) と明示する」という形で両者を区別できる、というのが狙いでした。
さらに AnyObject から Swift の値型へ一段で取り出したいケース([AnyObject] や [NSObject: AnyObject] の要素の取り出し)向けに、プロトコル拡張で汎用的な失敗可能イニシャライザを用意する案もありました。
extension _ObjectiveCBridgeable {
init?(bridging object: AnyObject) {
if let bridgeObject = object as? _ObjectiveCType {
var result: Self? = nil
if Self._conditionallyBridgeFromObjectiveC(bridgeObject, &result) {
self = result!
return
}
}
return nil
}
}
catch let x as NSError のように「Swift の任意エラーを NSError として扱いたい」用途が多い点については、ErrorProtocol 自身に domain / code / userInfo を生やして、わざわざ NSError に橋渡ししなくても必要な情報が取れるようにしておく、というフォローも同時に提案されていました。
extension ErrorProtocol {
var domain: String { return NSError(self).domain }
var code: Int { return NSError(self).code }
var userInfo: [NSObject: AnyObject] { return NSError(self).userInfo }
}
扱わないと明言されていた範囲
動的キャストに残る「言語の暗黙変換を追随する」挙動、すなわち Optional をくぐる挙動や Array / Dictionary / Set の共変な要素キャストについては、この提案の対象外 とされていました。これらはコンパイル時の暗黙変換ルールとセットで議論すべきという位置づけです。
却下の経緯
この提案は Swift 3 のレビュー期間に取り上げられたものの、レビュー中に Swift 3 への取り込みが defer(先送り) と判断され、後に先送りされた一連の提案をまとめて clean up する流れの中で最終的に Rejected となりました。そのため現在の Swift でも、
value as? NSStringやvalue as? Stringのような動的キャスト経由の橋渡し、catch let error as NSErrorによる Swift エラーからNSErrorへの変換、Array/Dictionary/SetとそのNS系クラスとのas?/as!キャスト
はいずれも生きており、イニシャライザ中心の API 体系には置き換わっていません。ただし提案の動機だった「as? / as! / is に橋渡しが混ざっていることの分かりにくさ」は今なお残っており、同種の議論が再度持ち上がったときの出発点として、この提案の整理は引き続き参照する価値があります。