Swift Digest
SE-0025 | Swift Evolution

Scoped Access Level

Proposal
SE-0025
Authors
Ilya Belenkiy
Review Manager
Doug Gregor
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 2時点のアクセス制御は public / internal / private の3段階で、private は「同じファイル内からアクセス可能」という意味でした。つまり、クラスや構造体の実装詳細を「その型の外からは絶対に触らせたくない」と表現する手段は、言語として用意されていませんでした。

ファイル分割でしか「型の外に出さない」を表現できない

private がファイル単位で効くため、実装詳細を本当に隠したい場合は、その型を単独のファイルに分けるしかありませんでした。すると、

  • 1ファイル1型という構成を事実上強いられ、関連する型を同じファイルにまとめて整理するといった、設計上自然なファイル構成が取りづらくなる
  • 逆に1つのファイルに複数の型が同居している場合、private が付いたメンバが「この型の外に出したくない」のか「同居している別の型には共有したい」のか、読み手には区別できない

という問題がありました。結果として、コードの物理的な配置(どのファイルに置いたか)によって、本来は型の設計意図として語るべき情報が暗黙的に表現されてしまっていました。

_ プレフィックスなどの慣習は強制力が無い

別の回避策として、公開したくないAPIの名前に _ を付けてコード補完などから「目立たなくする」慣習もありました。しかしこれはあくまで慣習であり、コンパイラは何も保証してくれません。呼び出し側が意図的にでも不注意にでもそのAPIを使えてしまうため、「内部用APIが公開APIと似た挙動を持つが、特定の前提のもとでしか安全でない」といったケースでは、誤用が実害につながるおそれがありました。

「型の内側だけで使うAPI」を宣言する語彙が無い

この提案の出発点は、「このメソッドやプロパティは、宣言されたクラスや拡張の中でしか使わないでほしい」という設計意図を、コンパイラに検査させる形で言語に書けるようにすることです。ファイル分割や命名規則ではなく、アクセス修飾子そのもので表現できるようにしよう、というのが本提案の狙いでした。

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

Swift 3では、アクセス修飾子を4段階に整理し直し、「宣言された字句スコープの中でだけ見える」という新しい段階を導入しました。あわせて、従来の「同じファイル内で見える」という意味は fileprivate に名前を譲り、private 自身はより狭い「宣言スコープの中だけ」を表すキーワードに切り替えられました。

4段階のアクセスレベル

修飾子 可視範囲
public モジュールの外からも見える
internal 同じモジュール内からのみ見える(デフォルト)
fileprivate 同じファイル内からのみ見える
private 宣言された字句スコープ(クラス本体や拡張の中)からのみ見える

Swift 2までの private に相当するのは fileprivate で、Swift 2の既存コードをSwift 3へ移行する際は、現状の意味を保ちたければ private を機械的に fileprivate に書き換えることになります(実際には、多くのケースでは新しい private の意味でもそのままコンパイル・動作します)。

private の効き方

private の付いたメンバは、宣言された型本体や拡張の「その字句スコープ」の外からは見えません。同じ型に対する別の extension からも見えないのがポイントです。

class A {
    private var counter = 0

    // counter を読み書きできる公開API
    func incrementCount() { counter += 1 }

    // この字句スコープの外からは見えない
    private func advanceCount(dx: Int) { counter += dx }
}

extension A {
    // ここからは counter も advanceCount も見えない

    // この extension の中だけで使うヘルパ
    private func incrementTwice() {
        incrementCount()
        incrementCount()
    }
}

同じ型の別の extension に置いた private メンバを共有したい場合は、private ではなく fileprivate を使うか、両者を同じ型本体にまとめる、という設計判断になります。

private な型のメンバの扱い

型自体に private を付けた場合、その型そのものは外から見えませんが、中のメンバは「型の外(ただし型を宣言している字句スコープの中)」から普通に使えるのが直感的です。これを実現するために、次のような細かいルール調整が行われています。

class Outer {
    private class Inner {
        var value = 0  // 明示しなくても internal 相当として扱われる
    }

    func test() {
        let inner = Inner()
        print(inner.value)  // Outer の中なので Inner と value の両方が見える
    }
}
  • どこでも、アクセス修飾子を書かなかった場合のデフォルトは internal です。private な型の中でも、メンバは明示しない限り internal 扱いになります。
  • 外側の型より広いアクセスレベルをメンバに書いても、従来のような警告は出さなくなりました。これは「将来この型をもっと広く公開するならこの粒度にしたい」という設計者の意図をそのまま書ける、というねらいです。型自体が private である以上、外から値が漏れてこないため、実際に可視になる範囲はあくまで型の可視範囲までに制限されます。
  • メンバの型は、「そのメンバが見える範囲」で参照できる宣言だけを使える、というルールに緩和されました。たとえば同じスコープに置いた privatetypealias を、同じスコープの private な型のプロパティの型として使う、といった書き方が許されます。
  • プロトコル要件を満たすメンバや required なイニシャライザは、外からの呼び出しが前提になるため private にはできません。

extension に付けたアクセス修飾子

extension 自体にアクセス修飾子を付けた場合、その拡張内のデフォルトアクセスレベルが指定された値に揃います。とくに private extension は「ファイルスコープで宣言された private」と同じ扱いになり、中のメンバのデフォルトは fileprivate になります。拡張に書いた修飾子より広いアクセスを個別メンバに指定した場合は、従来どおり警告の対象です。

使い分けの指針

4段階のアクセスレベルは、次のように整理して選ぶのが基本です。

  • モジュール外に公開したい … public
  • 同じモジュール内で共有したい(デフォルト) … internal
  • 同じファイル内の関連する型や拡張からは使いたいが、モジュール全体には出したくない … fileprivate
  • 宣言した型本体や拡張の中だけの、純粋な実装詳細 … private

private を使うと、ファイルを分割しなくても「この部品は型の中だけで閉じている」という意図をコンパイラに検査させられるため、関連する型を同じファイルにまとめる設計がしやすくなります。ファイル構成を言語が強制するのではなく、設計意図をアクセス修飾子で直接表現できるようになったのが、この提案の実質的な効果です。