Swift 3の変更点の裏側 (アクセス制御 / @escaping)


こんにちは。VASILYのiOSエンジニアのにこらすです。

2015年の12月からSwiftがオープンソースになり、 Swift Evolution(Swift言語の新しい仕様について提案する場所)で多くの開発者の提案が採用されました。
今回はSwift 3の アクセス制御 と @escaping についての変更点と、その背景について紹介します。

Swift 言語の変更点はすべて、Swift Evolution で確認することができます。
さらに変更点だけでなく、決定に至る議論の内容はSwift Evolution Mailing List のメーリングリストで追うことができます。さらにここで アーカイブ も読めます。

アクセス制御

Swift 2 でのアクセス制御の概念は private, internal, public の3つでした。
Swift 3では、Swift Evolution の下記の2つの提案が承認されました。

この変更でアクセスコントロールのルールに変更があり、新しいキーワード fileprivateopen が追加されました。

fileprivate SE-0025

SE-0025 が承認されたことで、新しいアクセス修飾子 fileprivate が追加されました。
その結果、Swift 3のアクセス制御ルールは下記の表のようになりました。

アクセス修飾子 意味 SE-0025で変更があったか
public モジュールの外からアクセス可能。クラスなら継承することもできる 変更なし
internal 同一モジュール内のならアクセス可能 変更なし
fileprivate 同一ファイル内ならアクセス可能 🆕
private 同一スコープ内のみアクセス可能 🆕

SE-0025では public, internal の意味は変わりませんが、新しくfileprivateという修飾子が追加されました。
fileprivate として定義されたものは同じファイルの中からどこでもそのメンバーにアクセスできます。これはSwift 2でのprivateと同じ挙動です。
一方 private の方は、同一ファイル内でも、クラスやextensionをまたぐなどスコープを超えたアクセスができなくなりました。

なぜ fileprivate が必要だったのか

Swift 3から private の意味はファイルに対してではなく、スコープに対して private になりました。
fileprivate は Swift 2 の private と同じです。
fileprivateprivate の区別ができると、 extension の中でのみ使用できるメソッドを追加出来るようになります。

例えば Stack というデータを保持するクラスを作るとき、push処理とpop処理 を別々の extension に書きたいことがあります。
このときpop() するときだけ、pop直前のデータをprintしたい。こういう時に extension の中で private が使えます。

class Stack<T> {
    fileprivate var elements = [T]()
}

// push extension
extension Stack {
    func push(_ element: T) {
        printStack()    // 呼び出せない
        elements.append(element)
    }
}

// pop extension
extension Stack {
    private func printStack() {
        print("Stack: \(elements)")
    }

    func pop() -> T? {
        printStack()
        return elements.removeLast()
    }
}

printStack() は pop extension の中だけで使いたいので、 private にして定義された extension の外からは呼び出せないようにできます。
printStack() はちゃんと隠れています。

private extensionfileprivate

Swift の extension を書くときに、 private extension とすると、その extension 内のメンバーは何も書かなければ fileprivate と同じアクセスレベルになります。
extension はそもそもファイルのトップレベルに宣言するもので、 private extensionprivate の有効範囲はファイル全体になります。
したがって プロパティ一つ一つに fileprivate を書くことと同じ動作をします。
下記のコードは一緒です。

fileprivateが付いてる書き方

class Hoge { }
extension Hoge {
    fileprivate func methodA() { ... }
    fileprivate func methodB() { ... }
    fileprivate func methodC() { ... }
}

暗黙的な書き方

class Hoge { }
private extension Hoge {
    func methodA() { ... }
    func methodB() { ... }
    func methodC() { ... }
}

open SE-0117

SE-0117 が承認されたことで、新しいアクセス修飾子 open が追加されました。
その結果、Swift 3のアクセス制御ルールは下記の表のようになりました。

アクセス修飾子 意味 SE-0117で変更があったか
open モジュールの外からアクセス可能。クラスなら継承することもできる (Swift 2 での public と同じ挙動) 🆕
public モジュールの外からアクセス可能。 外部からクラスの継承はできない 🆕
internal 同一モジュール内のならアクセス可能 変更なし
fileprivate 同一ファイル内ならアクセス可能 変更なし
private 同一スコープ内のみアクセス可能 変更なし

なぜ open が必要だったのか

他の言語にある多彩なアクセスコントロールではできることが、Swift 2ではフルオープンな public しかありませんでした。
オープンソースライブラリを公開するときなど、クラスを公開はしても、継承はしてほしくないときに利用者に明示的にその意志を伝える事ができます。

Swift 2 の final public と Swift 3 の public

Swift 2 の public final と Swift 3 の public は似たような振る舞いをしますが、厳密には下記のような違いがあります。

// Swift 2
public final class Mammal { }
public class Dog: Mammal { }    // 同一モジュール内でも継承できない
// Swift 3
public class Mammal { }
public class Dog: Mammal { }    // 同一モジュール内では継承可能

@escaping SE-0103

Swift 3 から @noescape の言語キーワードが無くなり、メソッドのクロージャー引数はデフォルトで離脱しない @noescape と同じ挙動になりました。
一方、メソッドのクロージャー引数が離脱する「可能性がある」場合、 @escaping キーワードを使う必要があります。
UIKit にもアニメーション処理など @escaping が付いてるメソッドが多いので、ぜひとも理解しておきたい概念です。
( @noescape : 「離脱しない」、 @escaping: 「離脱する」と表現しています)

離脱しないクロージャー

Swift 3のデフォルトのクロージャー引数は、すぐに破棄されるため、[weak hoge] を書かなくても循環参照することはありません。

class Hoge {
    func useIt(thisClosure: () -> Void) {
        thisClosure()
    }
}
let hoge = Hoge()
// デフォルトで離脱しないクロージャーなので、[weak hoge] を書かなくても循環参照にならない。
hoge.useIt() { print(hoge) }

離脱するクロージャー実例

下記のコードのように引数のクロージャー ( thisClosure ) をプロパティにコピーすると、メソッド実行後にも引数のクロージャーが破棄されない (離脱する) ため、コンパイルエラーが発生します。

// コンパイルエラー
var myPrettyClosure: (() -> Void)? = nil
func trapIt(thisClosure: () -> Void) {
    thisClosure()
    myPrettyClosure = thisClosure
}

クロージャーの型宣言の前に @escaping を書くことでコンパイルが通るようになります。

// OK
var myPrettyClosure: (() -> Void)? = nil
func trapIt(thisClosure: @escaping () -> Void) {
    thisClosure()
    myPrettyClosure = thisClosure
}

なぜクロージャーのデフォルトの挙動が変わったのか

Swift 2以前のクロージャーでは、強く意識していないと容易に循環参照が発生しがちです。
Swift 3では、この循環参照が発生しにくくなるように、クロージャーはデフォルトで離脱しない ( @noescape ) ようになりました。
Swift でのクロージャーは、map, filter, reduce に代表される関数型プログラミングのような離脱しない ( @noescape ) 使い方の方が多く存在します。

まとめ

今回、Swift 3のアクセス制御と @escaping について説明しました。
しかし、Swift 3にはまだ大きな破壊的変更が多く存在します。
fileprivate, private のおかげでもっと徹底的なモジュールアクセス制御ができるようになり、open, public のおかげで外からアクセスできるクラスが継承可能かどうかを区別できるようになりました。
次回のブログでもまた Swift Evolution の他の変更点に紹介したいと思います。
ー にこらす