[iOS 8][Swift] アクセス修飾子を理解する

Swiftはアクセス修飾子(Access Control)が利用できます。これにより式やクラスのスコープを明確に定義することができます。
すなわち、「変数」「定数」「クラス」「メソッド」「関数」といったプログラミングを構成する各要素へのアクセス権限を制御できるようになっています。

ただ、このアクセス修飾子がJavaなどと同じ感覚で理解しようとすると「?」となる部分が多いのでまとめておきます。

アクセス修飾子

Swiftのアクセス修飾子は3つのレベルで定義しています。

public

誰からでもアクセスすることができます。APIを定義する時などに利用されます。
異なるファイルでも異なるモジュールであってもアクセスすることが出来ます。
Javaの public とは少々意味が異なり、特殊な用途(ライブラリのAPI定義など)に利用されるアクセス修飾子となります。
Javaにおける public より広範囲にアクセスを許可するようなイメージになります。そのため、Javaには同じ概念は存在しません。名称が同じなので、特に注意が必要です。

internal

同じモジュール内であればアクセスすることができます。
一つのプロジェクト内のターゲットが同一であれば、 public と同じように誰でもアクセスすることができます。ターゲットが異なると参照することができません。

private

同じファイル内からのアクセスのみ許可されています。クラス単位のスコープではないことに注意が必要です。

わけわからん・・・

概略を言葉だけで表すとイマイチピンと来ないので概略図を作ってみました。赤い線がアクセス不可、緑の線がアクセス可能 を示します。

access_control2

何となく見えてきました。次のセクションからサンプルを中心に確認してみます。

internal

Swiftにおいてのデフォルトのアクセス権限となります。
基本的に internal は、Javaにおける public と同じ意味と捉えて問題ありません。
単一のプロジェクト内の同一ターゲットのソースコードからは、全てアクセス可能になっているため、
通常のプログラミングにおいては、このスコープで問題になることはないでしょう。

internal で宣言したオブジェクトは、別のモジュールとして利用する場合にアクセスが制限されます。
読み込んだモジュール内部の実装は不可視となります。そのため、ライブラリのAPIなどを用意したい場合には、
後述する public というアクセス修飾子を明示的に指定する必要があります。

モジュールって何?
モジュールとは import して読み込んで利用するFrameworkを作成するための仕組みです。Javaでいうjarのようなものをイメージしてます。多分近いものでしょう。

サンプルコードで確認

class Book {
  let name:String  
  init(name: String) {
    self.name = name
  }
}

internal class BookStore {  
  // Book1
  let book1 = Book(name: "Book 1")
  // Book2
  let book2 = Book(name: "Book 2")

  func getAllBooksName() -> (String, String) {
      return (book1.name, book2.name)
  }
}

// 同一ファイル内からアクセスすることができる
let bookStore = BookStore()

// internal methodへのアクセス
bookStore.getAllBooksName()    // OK

// internal valueへのアクセス
bookStore.book1             // OK

サンプルコードを確認します。クラス定義には明示的に internal を記述しました。 この記述は省いてしまっても同じく internal として動作します。
book1 , book2 , func getAllBooksName() は、何もアクセス修飾子が付与されていません。 これも internal として自動的に定義されます。

ここで定義した BookStoreクラス は、同じモジュールからの利用であれば、特に意識せずにpublicと同じ感覚で利用できます。そのため、同じモジュール内であれば別ファイルから呼び出すことが可能です。

// 別ファイルからでもアクセスができる
let bookStore1 = BookStore()

// internal methodへのアクセス
bookStore1.getAllBooksName()    // OK

// internal valueへのアクセス
bookStore1.books1             // OK

private

private は、Swiftにおいて一番制約の厳しいアクセス修飾子です。ファイル外のコードから、クラスやそのメンバー、エクステンション、トップレベルの関数などに対してアクセスできないようにします。

注意すべきは private で宣言した場合にクラス単位ではなく、ファイル単位のアクセス制御である点です。Javaと同じ感覚で private を利用すると「なぜこのクラスや値が見えているのか?」と途惑うことが多々あります。

クラスを private にしてしまうと、誰からも見えない不可視のクラスになると考えがちです。
しかしSwiftでは、同じファイル内のクラスからはアクセスが可能です。private はファイルをまたいだアクセスを行うときに、初めて不可視になります。

サンプルコードで確認

// ファイル単位でprivate
private class SecretBook {

    init(title: String) {
      self.title = title
    }

    // タイトル
    let title: String

    func description() -> String {
      return "\(title) is Secret Book"
    }
}

internal class BookStore {
  // 同じファイルの中であればprivateクラスを呼び出せる
  private let secretBook = SecretBook(title: "Secret Book")
}

上記のようなサンプルコードで確認してみましょう。

privateを利用する上での注意

注意すべき点がひとつあります。これは private クラスで定義されたクラスを internal の値として定義しています。詳しくは後述します。

以下のコードではコンパイルエラーとなります。

internal class BookStore {
  // この値のクラスはprivate
  let secretBook = SecretBook(title: "Secret Book")     // コンパイルエラー
}

SecretBookクラス は private で宣言されているので、他のファイルからのアクセスを拒否しています。それを BookStore の中で、他ファイルから許可するよう internal で指定しています。
当然ながら、この secretBook のクラス定義は参照できないため利用することが出来ません。

public

Swiftにおけるpublicアクセス修飾子の役割は、ちょっと特殊です。先述した概略図の通り、モジュールとして取り込んだものでもアクセス可能になります。そのため、ライブラリのAPIなどの定義に使われます。

public class BookStore {
  // 初期化
  public init() {
    // 処理
  }
}

public を付与したことで、 BookStore クラスは誰でもアクセスが可能になりました。ライブラリのAPIなど全てに公開する情報は明示的に public にする必要があります。

注意すべきは、 init() です。これも明示的に public を付与する必要があります。そうしないと、モジュールの外部から利用した時にイニシャライザーが不可視となってしまい、インスタンス化できません。

アクセス修飾子の上書き(?)

前述で何度か出てきていますが、private で定義されたクラスを変数または定数として宣言する場合、publicinternal とすることはできません。

private class SecretMessage {
  init() {}
}

internal class MessageBox {
  let message1 = SecretMessage()   // エラー
}

スクリーンショット 2014-11-24 23.42.50

Swiftではこのコードはエラーとなります。SecretMessage というクラスは、private で宣言されているため、同じファイル内でのアクセスしか許可されていません。そのため、他ファイルからアクセスしようとしても、クラスの定義が不可視であるため、利用が禁じられています。

一方、MessageBox というクラスの message1 という定数は、internal で定義されています。そのため、この message1 は他ファイルからもアクセスが可能なはずです。
しかし、SecretMessage は他のファイルからは見えないことになっています。この時点で矛盾しています。よってこのコードは、エラーとなります。

まとめ

自分自身がJavaの感覚が強いため、Swiftのアクセス修飾子の特徴である「ファイルスコープ」には違和感が強かったのですが、iBooksの公式ドキュメントやら自分でサンプルを書いてみて、少し理解できたような気がします。
おそらくこの辺りは、実際にiOSアプリを開発する上で結構意識しなければならない箇所かと思うので(全部publicで宣言してれば別ですが、それは論外)、まとめてみました。参考になれば幸いです。