Class and Subtype existentials
01 何が問題だったのか
Swift 3 時点では、existential(存在型)として表現できるのはプロトコル合成 Protocol1 & Protocol2 に限られていました。「あるクラスまたはそのサブクラスであり、かつ特定のプロトコルに適合している」という型を Swift の型システム上で直接書き表せなかった、ということです。
一方で Objective-C には次のような構文があり、クラス制約とプロトコル制約を組み合わせた existential を扱えます。
id<Protocol1, Protocol2> // Protocol1 と Protocol2 に適合するクラス型
Base<Protocol>* // Base のサブクラスで Protocol に適合する型
この種の型は Cocoa / UIKit の API で広く使われており、典型的には次のような形で登場します。
- (void)setup:(nonnull UIViewController<UITableViewDataSource, UITableViewDelegate> *)tableViewController;
「UIViewController のサブクラスであって、同時に UITableViewDataSource と UITableViewDelegate に適合していること」を要求するシグネチャです。Swift 3 にはこれに対応する書き方がなかったため、Swift 側では次のようにクラス制約が落とされた状態でインポートされていました。
class MyViewController {
func setup(tableViewController: UIViewController) {}
}
この結果、プロトコル適合の要件が型レベルで消えてしまい、プロトコルのメソッドを実装していない UIViewController をそのまま渡せてしまいます。
let myViewController = MyViewController()
myViewController.setup(UIViewController()) // コンパイルは通るが、Objective-C 側から
// デリゲートメソッドが呼ばれた瞬間にクラッシュ
また、Swift のみで書くコードであっても、「UIView のサブクラスで Themeable にも適合している値」のような自然な要件を型として表現する手段がありませんでした。
02 どのように解決されるのか
プロトコル合成の & 構文を拡張し、要素としてクラス型や AnyObject を書けるようにします。これにより「クラス制約 + プロトコル適合」の existential を Swift 自身の型として表現できるようになります。
AnyObject & Protocol1 & Protocol2 // Protocol1, Protocol2 に適合するクラス型
Base & Protocol // Base のサブクラスで Protocol に適合する型
AnyObject をクラス制約として書ける
合成の要素に AnyObject を書くと、そこに class 制約が入ります。構造体などの値型は代入できません。
protocol P {}
struct S: P {}
class C: P {}
class D {}
let t: AnyObject & P = S() // エラー: S はクラス型ではない
let u: AnyObject & P = C() // OK
let v: P & AnyObject = C() // OK(要素の順序は自由)
let w: P & AnyObject = D() // エラー: D は P に適合していない
クラス型を書くとサブタイプ制約になる
合成の要素にクラス型を書くと、そのクラスまたはそのサブクラスでなければならない、という制約になります。
protocol P {}
struct S {}
class C {}
class D: P {}
class E: C, P {}
let u: S & P // エラー: S はクラス型ではない
let v: C & P = D() // エラー: D は C のサブクラスではない
let w: C & P = E() // OK: E は C のサブクラスで、かつ P に適合
クラス制約と AnyObject の同居はクラス側が優先される
合成に AnyObject とクラス型の両方が現れた場合、より強い制約であるクラス型の方が採用されます。
protocol P {}
class C {}
class D: C, P {}
let u: AnyObject & C & P = D() // OK
let v: C & P = u // OK(C & P と AnyObject & C & P は等価)
let w: AnyObject & C & P = v // OK(同上)
クラス型が二つ以上ある場合
合成にクラス型が複数現れた場合は、どちらか一方がもう一方のサブクラスでなければなりません。その場合はより具体的(サブクラス側)の制約に正規化されます。互いに無関係なクラス同士を合成しようとするとエラーになります。
protocol P {}
class C {}
class D: C {}
class E: C {}
class F: D, P {}
let t: C & D & P = F() // OK: C & D & P は D & P に正規化される
let w: D & E & P // エラー: D と E は互いのサブクラスではない
typealias は展開してから判定する
typealias を含んだ合成は、まず各 typealias を展開し、同じクラス制約や AnyObject をまとめたうえで、上記のルールで妥当性を判定します。
class C {}
class D: C {}
class E {}
protocol P1 {}
protocol P2 {}
typealias TA1 = AnyObject & P1
typealias TA3 = C & P2
typealias TA4 = D & P2
typealias TA5 = E & P2
typealias TA6 = TA1 & TA3 // 展開後: C & P1 & P2(AnyObject はクラス型に吸収)
typealias TA7 = TA3 & TA4 // 展開後: D & P2(C < D の関係でサブクラス側に寄る)
typealias TA8 = TA4 & TA5 // エラー: D と E が両立しない
class と AnyObject の統合
これまで別物として存在していた class 制約と AnyObject は、どちらも「クラス型の existential」を表す同じ概念として統合され、AnyObject に一本化する方向になります。class は当面は AnyObject のエイリアスとして残り、将来的に廃止される想定です。
継承節と typealias の関係
プロトコル合成をクラスの継承節にそのまま書くことは引き続きできません。また、クラス型を含む typealias を継承節で使ったとしても、そのクラスを暗黙には継承しません。継承関係は明示的に書く必要があります。
protocol P1 {}
class C {}
class D: P1 & P2 {} // エラー: 継承節で & は使えない
class E: C & P1 {} // エラー: 同上
typealias CP1 = C & P1
class E: CP1 {} // エラー: C を暗黙継承しない
class E: C, CP1 {} // OK: 継承は明示的に書く
Objective-C からのインポートへの影響
この拡張により、冒頭で触れた Objective-C の UIViewController<UITableViewDataSource, UITableViewDelegate> * のような型は、Swift 4 モード以降では次のように忠実にインポートされるようになります。
class MyViewController {
func setup(tableViewController: UIViewController & UITableViewDataSource & UITableViewDelegate) {}
}
結果として、プロトコル適合を満たさない UIViewController を渡すコードは呼び出し時点でコンパイルエラーとなり、実行時クラッシュを型レベルで防げるようになります。