こんにちは。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つの提案が承認されました。
- Scoped Access Level SE-0025
- Allow distinguishing between public access and public overridability SE-0117
この変更でアクセスコントロールのルールに変更があり、新しいキーワード fileprivate
と open
が追加されました。
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
と同じです。
fileprivate
と private
の区別ができると、 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 extension
と fileprivate
Swift の extension
を書くときに、 private extension
とすると、その extension
内のメンバーは何も書かなければ fileprivate
と同じアクセスレベルになります。
extension
はそもそもファイルのトップレベルに宣言するもので、 private extension
の private
の有効範囲はファイル全体になります。
したがって プロパティ一つ一つに 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
の他の変更点に紹介したいと思います。
ー にこらす