Swift Digest
SE-0117 | Swift Evolution

Allow distinguishing between public access and public overridability

Proposal
SE-0117
Authors
Javier Soto, John McCall
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

これまでの Swift では、クラスやそのメンバーに public を付けると、次の2つの能力がひとまとめに与えられていました。

  • 他モジュールから 利用 できる(インスタンス化する、メソッドを呼ぶ、プロパティを読み書きする)
  • 他モジュールから サブクラス化 / オーバーライド できる

しかしライブラリ設計の観点からは、この2つは本来別物です。ライブラリの作者がクラスを public にする目的の多くは、単に「使ってほしい」だけであって、「好きにサブクラス化して挙動を差し替えてほしい」とまでは考えていないことがほとんどです。にもかかわらず public にした時点で後者まで許してしまうと、次のような問題が生じます。

  • 後方互換の制約が増える: サブクラスはスーパークラスのメソッド同士の呼び出し順序などの実装詳細に依存しがちなので、内部で self.foo() から self.bar() を呼ぶような委譲の書き方を変えるだけでも、外部のサブクラスを壊してしまう可能性があります。クラス外から安全にサブクラス化できるように設計するには、通常の利用向けに設計するよりもはるかに慎重な検討(どのメソッドがどの順に呼ばれる、という契約の明文化など)が必要です。
  • 最適化が効きにくくなる: Swift は静的コンパイル言語であり、クラスメソッドの多くは実際にはオーバーライドされません。オーバーライドされないとわかれば仮想呼び出しを直接呼び出しに落とす devirtualization が効き、さらに呼び出し元と呼び出し先をまたいだ最適化の糸口にもなります。外部からのオーバーライドを常に許してしまうと、この最適化の前提が崩れ、バイナリサイズや起動時間、実行速度にじわじわと不利を与えます。

progressive disclosure の観点でも、「ただ public にして使わせたいだけ」のクラスに、外部からサブクラス化される可能性を前提とした注釈や性能上のペナルティがもれなく付いてくるのは望ましくありません。

また、すべての public クラスを final 相当にしてサブクラス化自体を封じる、という方針も単純には採れません。final はいちど付けると取り消せない約束であり、「将来サブクラス化を許すかどうかは後で決めたい」という選択肢をライブラリの作者から奪ってしまうからです。必要なのは、「利用できる」ことと「サブクラス化 / オーバーライドできる」ことを分けて表現できる ようにする仕組みです。

02 どのように解決されるのか

新たなアクセス修飾子 open を導入し、モジュールをまたいだサブクラス化 / オーバーライドを明示的にオプトイン する形に変更します。openpublic よりも一段階許可範囲が広いアクセスレベルという位置づけで、他のアクセス修飾子と同時には指定できません(public open のような書き方はできません)。適用できる対象は クラスと、オーバーライド可能なクラスメンバー(メソッド・プロパティ・サブスクリプト) に限られます。

publicopen の関係

  • public class C { ... }: モジュール外から 利用 はできるが、サブクラス化はできない。
  • open class C { ... }: モジュール外から利用もサブクラス化もできる。
  • public func foo(): モジュール外から呼び出せるが、オーバーライドはできない。
  • open func foo(): モジュール外から呼び出しもオーバーライドもできる。

open クラスは final にはできません。また open クラスのスーパークラスも open である必要があります(モジュール外から open クラスを作るには、その基盤となるスーパークラスもモジュール外からサブクラス化可能でなければならないため)。

イニシャライザはこの仕組みから外れていて、open を付けることはできません。サブクラス側のイニシャライザはスーパークラスのそれとは論理的に別のインターフェースなので、署名が一致していても自由に宣言できます(required なイニシャライザも同様)。

使用例

モジュール A 側の宣言は次のようになります。

// ModuleA

// このクラスは ModuleA の外からはサブクラス化できません。
public class NonSubclassableParentClass {
    // このメソッドは ModuleA の外からはオーバーライドできません。
    public func foo() {}

    // open を付けることは可能ですが、クラス自体が public で
    // あるため、結局モジュール外からはオーバーライドできません。
    open func bar() {}

    // final の挙動は従来どおりです。
    public final func baz() {}
}

// このクラスはモジュール内でも外でもサブクラス化できます。
open class SubclassableParentClass {
    // このプロパティは ModuleA の外ではオーバーライドできません。
    public var size: Int = 0

    // このメソッドは ModuleA の外ではオーバーライドできません。
    public func foo() {}

    // このメソッドはモジュール内でも外でもオーバーライド可能です。
    open func bar() {}

    // final の挙動は従来どおりです。
    public final func baz() {}
}

// final クラスの挙動も従来どおりです。
public final class FinalClass {}

モジュール B からの利用側は次のようになります。

// ModuleB
import ModuleA

// スーパークラスが public どまりなので、これはコンパイルエラーです。
class SubclassA: NonSubclassableParentClass {}

// スーパークラスが open なので、これは許されます。
class SubclassB: SubclassableParentClass {
    // foo は public なので、モジュール外からのオーバーライドはエラーです。
    override func foo() {}

    // bar は open なのでオーバーライドできます。
    // SubclassB 自体は internal なので、このオーバーライドに open は不要です。
    override func bar() {}
}

open class SubclassC: SubclassableParentClass {
    // open なクラスの中で open メソッドをオーバーライドしているのに
    // 自分は open 宣言されていないため、これはエラーです。
    override func bar() {}
}

open class SubclassD: SubclassableParentClass {
    // これは有効です。
    open override func bar() {}
}

open class SubclassE: SubclassableParentClass {
    // final を付ける場合は、open の代わりに public でも構いません。
    public final override func bar() {}
}

ポイントは次のとおりです。

  • 別モジュールのクラスをサブクラス化するには、そのクラスが open でなければなりません。
  • 別モジュールのメンバーをオーバーライドするには、そのメンバーが open でなければなりません。
  • open クラスの中で open メンバーをオーバーライドするときは、オーバーライド側も open にする必要があります。例外として、final を付ける場合や final クラス内でのオーバーライドの場合は、代わりに public と書くことができます。
  • open メンバーを継承したサブクラスでは、特に宣言しなくてもそのメンバーは引き続き open として扱われます(クラス自体が final な場合を除く)。

Objective-C との相互運用

Objective-C からインポートされるクラスとメソッドはすべて open として扱われます。これは Objective-C のクラスが実行時にいつでもサブクラス化・メソッド差し替えを許してきた経緯に合わせるためで、Objective-C クラスの Swift 側ヘッダでは public が一様に open に置き換わることになります。dynamic なメンバーについても、Objective-C ランタイムを通じて置き換えられ得るため、原則として open として宣言するのが自然です。

@testable の扱い

@testable import によってテストモジュールに与えられる追加の権限は従来どおり維持され、@testable 先のモジュールに含まれる非 finalinternal / public クラスやメソッドは、これまでと同じようにサブクラス化・オーバーライドできます。

既存コードへの影響

この変更は、publicfinal なクラスやメソッドを外部モジュールからサブクラス化・オーバーライドしている既存のコードに対しては、そのままではコンパイルが通らなくなる破壊的な変更です。そうした API を維持したい場合は、ライブラリ側でスーパークラスとメンバーを open に書き換える必要があります。Swift 3 への移行においては、マイグレータによる open への変換が想定されています。