Improve Interaction Between private Declarations and Extensions
01 何が問題だったのか
Swift 3 では SE-0025 によってアクセス制御が再設計され、private は「宣言されたスコープ内からのみアクセス可能」というスコープベースの意味に変わりました。そのため、ある型に private で定義されたプロパティやメソッドは、同じ型の内側からはアクセスできても、その型の extension からはアクセスできません。
Swift では extension を「型の実装を論理的にグルーピングするための手段」として使うのが一般的で、conditional conformance のように extension が必須になる場面もあります。しかし SE-0025 のモデルでは、本体と extension に実装を分けた途端に private メンバを共有できなくなるため、実質的に fileprivate を使うしかなくなり、当初想定されていた「fileprivate はまれにしか使わない」という設計意図から外れてしまいました。
// Swift 3 での振る舞い
struct S {
private var p: Int
func f() {
use(g()) // エラー: g() は private で、extension の外からは見えない
}
}
extension S {
private func g() {
use(p) // エラー: p は private で、extension の中からは見えない
}
}
この状況を解消するために SE-0159 では「Swift 2 のアクセス制御に戻す」という大きめの変更が提案されましたが、スコープベースの private を価値あるものとして使っている開発者が多いことや、Swift 4 のソース互換性目標から見て変更が大きすぎることを理由に rejected となりました。そこで、SE-0025 の設計思想をほぼ保ったまま extension との相互作用だけを現実的に改善する、より小さな変更が必要とされました。
02 どのように解決されるのか
アクセス制御の観点で、同じファイル内にある型 T の本体と、その T に対する extension 群をひとつのスコープとして扱う ように変更されました。これにより、Swift 3 では fileprivate を使わざるを得なかった多くのケースで、private のままで済むようになります。
この変更の効果は次の2点です。
- extension 内の宣言から、型本体の
privateメンバにアクセスできます。 - extension 内で
privateとして宣言したメンバに、型本体や(同じファイル内の)他の extension からアクセスできます。
struct S {
private var p: Int
func f() {
use(g()) // OK: g() は S のスコープから見える
}
}
extension S {
private func g() {
use(p) // OK: extension からも p にアクセスできる
}
}
extension S {
func h() {
use(g()) // OK: 他の extension の private メンバにもアクセスできる
}
}
適用範囲の詳細
この可視性の拡張は、あくまで「同じファイル内の型とその extension」という範囲に限定されます。
- サブクラスには及びません。 同じファイルにあっても、サブクラスはスーパークラスの
privateメンバにアクセスできません。対象はあくまで型そのものの extension です。 - constrained extension も対象です。 たとえば
Optionalが定義されているファイル内にextension Optional where Wrapped == String { ... }を書いた場合、その extension はOptionalのprivateメンバにアクセスできます。 - 型がネストしている場合の扱いは変わりません。 Swift 3 と同様に、内側の型は外側の型の
privateメンバにアクセスできますが、外側の型は内側の型のprivateメンバにはアクセスできません。
型の定義されたファイルと別のファイルに extension がある場合、その extension 群は「別ファイルにまとめられたひとつのスコープ」として扱われます。同じファイル内の extension 同士では private メンバを共有できますが、型本体が別ファイルにある以上、型本体の private メンバにはアクセスできません。
// FileA.swift
struct A {
private var aMember: Int
}
// FileB.swift
extension A {
private func foo() {
bar() // OK: 同じファイル内の extension の private メンバは見える
}
}
extension A {
private func bar() {
aMember = 42 // エラー: private メンバは宣言ファイルの外からは見えない
}
}
移行時の注意
Swift 3 互換モードでは従来どおりの private として振る舞い、Swift 4 モードでこの新しい意味が適用されます。可視性が広がる方向の変更なので、基本的にマイグレーションは不要です。
ただし、同じ型の本体と extension のそれぞれに、同一シグネチャの private 宣言を別々に書いていた場合は、Swift 4 モードで Invalid redeclaration of '...' エラーになります。Swift 3 ではスコープが分かれていたため共存できていましたが、Swift 4 ではひとつのスコープに統合されるためです。
struct Foo {
private func bar() {}
}
extension Foo {
private func bar() {} // Swift 4 ではエラー: 再宣言とみなされる
}