Swift Digest
SE-0169 | Swift Evolution

Improve Interaction Between private Declarations and Extensions

Proposal
SE-0169
Authors
David Hart, Chris Lattner
Review Manager
Doug Gregor
Status
Implemented (Swift 4.0)

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 は Optionalprivate メンバにアクセスできます。
  • 型がネストしている場合の扱いは変わりません。 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 ではエラー: 再宣言とみなされる
}