Swift Digest
SE-0083 | Swift Evolution

Remove bridging conversion behavior from dynamic casts

Proposal
SE-0083
Authors
Joe Groff
Review Manager
Chris Lattner
Status
Rejected

01 何が問題だったのか

Swift の動的キャスト as? / as! / is は、本来「値の動的な型をチェックして、合致していればその型として取り出す」ための機能です。ところがこれらの演算子には、純粋な型チェックに加えて Cocoa の橋渡し変換(bridging conversion) までこっそり組み込まれていました。たとえば Any に入った StringNSString にキャストすると、型チェックではなく「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系統の仕事を抱えていました。

  1. 純粋な型チェック
    • クラスインスタンスが特定のクラスか確認する
    • existential(protocol 型の値)が特定の型を包んでいるか確認する
    • ジェネリックな値を別の具体型として扱えるか確認する
    • 値の型がプロトコルに適合しているか確認し、適合していれば existential に包み直す
  2. Cocoa の橋渡し変換
    • 値が _ObjectiveCBridgeable に適合していれば、対応する Cocoa クラスへ変換する(およびその逆)
    • ErrorProtocol(現在の Error)適合型を NSError へ橋渡しする
    • NSError を、ドメインとコードが一致する Swift エラー型へ復元する(_ObjectiveCBridgeableErrorProtocol 経由)
  3. 言語の暗黙変換を動的に引き継ぐ挙動
    • Optional をくぐり抜けて中身にキャストを試みる
    • Array / Dictionary / Set の要素に対してキャストを波及させる(共変な要素変換)

このうち 2 は、他の言語機能とは性格がかなり異なります。型チェックではなく「値を別の表現に作り直す」動作だからです。

混ざっていることで何が困るのか

役割が混在していることで、次のような問題が生じていました。

  • 挙動の驚き: as? / as! は「型を確認するだけ」と思って書くと、Cocoa 型が絡んだ瞬間に値変換まで走ってしまう。どこで安価な型チェックが終わり、どこから重い橋渡しが始まるのかが読めません。
  • パフォーマンス上の不透明さ: 動的キャストの中に橋渡し処理が隠れるため、as? 一つのコストが型の組み合わせで大きく変わります。
  • 値型とクラスの特別扱い: StringNSString のような 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! / isString / NSString 間の橋渡しや、ErrorNSError の橋渡しを従来どおり担っています。

それでも、提案の要点を押さえておくと、「なぜ動的キャストに橋渡しが混ざっているのか」「将来また議論になりそうな論点は何か」を理解する助けになります。

提案されていた内容(採用されなかったもの)

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? NSStringvalue as? String のような動的キャスト経由の橋渡し、
  • catch let error as NSError による Swift エラーから NSError への変換、
  • Array / Dictionary / Set とその NS 系クラスとの as? / as! キャスト

はいずれも生きており、イニシャライザ中心の API 体系には置き換わっていません。ただし提案の動機だった「as? / as! / is に橋渡しが混ざっていることの分かりにくさ」は今なお残っており、同種の議論が再度持ち上がったときの出発点として、この提案の整理は引き続き参照する価値があります。