# クロージャとは
クロージャとは、変数をスコープ内に閉じ込めるためのデータ構造です。関数はクロージャの一種で、使い方や機能は似ています。Swiftでは、func キーワードで関数を定義しますが、クロージャには クロージャ式 が提供されており、クロージャを簡潔に書くことができます。
# クロージャ式の定義
基本的なクロージャ式は次のフォーマットで書くことができます。
{ (引数名:引数の型) -> 戻り値の型 in
// 処理の実行
}
関数の書き方と似ていますが、クロージャ式は in
キーワードを使います。
次の例では、変数にクロージャを代入しています。代入された変数は、関数として実行することができます。
let sum = { (x: Int, y: Int) -> Int in
return x + y
}
let a = sum(1, 2) // 3
関数と同様に戻り値しかない場合は、暗黙的なreturn が有効になります。
let sum = { (x: Int, y: Int) -> Int in
// return を省略
x + y
}
let a = sum(1, 2) // 3
また、クロージャの型は通常のString型やInt型のように変数や引数の宣言時に使用することができます。
// クロージャの型を宣言
var closure: (Int) -> Int
// 宣言された型にマッチするクロージャを代入
closure = { (x: Int) -> Int in
return x + 1
}
# 型の省略
クロージャ式の引数と戻り値は、Swiftの型推論によって省略できるケースがあります。型を省略できるパターンは、予め型を指定している変数にクロージャを代入するときなどです。
次の例では、Int型を引数にとり、Int型を返すクロージャ型を定義しています。
var closure: (Int) -> Int
closure = { (x: Int) -> Int in
return x + 1
}
これを、型を省略して書くと次のようになります。
var closure: (Int) -> Int
// (x: Int) -> Int を省略
closure = { x in
return x + 1
}
変数にすでに型が定義されているので、Swiftが引数の型と返り値の型を推論できるのです。
# クロージャ式の引数
クロージャ式は関数と似ていますが、利用可能な引数が少し異なります。
以下は利用可能な仕様の比較です。
仕様 | 関数 | クロージャ式 |
---|---|---|
外部引数名 | ✅ | ✖️ |
デフォルト引数 | ✅ | ✖️ |
inout引数 | ✅ | ✅ |
可変長引数 | ✅ | ✅ |
簡略引数名($) | ✖️ | ✅ |
# 外部引数名
クロージャ式は、外部引数名は指定することができません。内部引数の定義のみになります。
そのため、呼び出し元は引数名を指定する必要がありません。
let sum = { (x: Int, y: Int) -> Int in
return x + y
}
// 引数名を指定しない
let a = sum(1, 2) // 3
# デフォルト引数
クロージャ式は、デフォルトの引数を指定することができません。
デフォルトの引数を指定しようとすると、コンパイラはエラーを出力します。
let sum = { (x: Int = 1, y: Int = 2) -> Int in
return x + y
}
let a = sum(1, 2)
/* 実行結果 */
// error: default arguments are not allowed in closures
// let sum = { (x: Int = 1, y: Int = 2) -> Int in
// error: default arguments are not allowed in closures
// let sum = { (x: Int = 1, y: Int = 2) -> Int in
# inout引数
クロージャ式は、inout引数 を指定する事ができます。書き方は、関数の宣言時と同じです。
let sum = { (int: inout Int) in
int = int + int
}
var val: Int = 1
sum(&val)
print(val) // 2
# 可変長引数
クロージャ式は、可変長引数 を指定する事ができます。書き方は、関数の宣言時と同じです。
let closure = { (animals: String...) in
for animal in animals {
print(animal)
}
}
closure("cat")
// cat
closure("cat", "dog", "rabbit")
// cat
// dog
// rabbit
# 簡略引数名
上記で 型の省略 について説明しましたが、引数も同様に簡略した書き方ができます。簡略引数は、$
に引数のインデックスをつけて使うことができます。
クロージャの型を定義した変数に、クロージャ式を代入します。通常の書き方だと次のようになります。
var closure: (Int, Int) -> Int
closure = { (x: Int, y: Int) -> Int in
return x + y
}
次に、型を省略して書いてみましょう。
var closure: (Int, Int) -> Int
// 型を省略
closure = { (x, y) in
return x + y
}
さらに、簡略引数名を使用してみます。簡略引数名を使用すると、1番目の引数(x)が $0
になり、 2番目の引数(y)が $1
になります。
var closure: (Int, Int) -> Int
// 1番目の引数が $0
// 2番目の引数が $1
closure = {
return $0 + $1
}
さらにこの場合は、暗黙的なreturnも有効なので、最終的にこのように書くことができます。
var closure: (Int, Int) -> Int
// return を省略
closure = { $0 + $1 }
# トレイリングクロージャ
トレイリングクロージャとは、関数の引数のクロージャを ()
の外に記述できる記法です。引数の最後が、クロージャ型の場合のみ書くことができます。
通常、トレイリングクロージャを使用せずにクロージャを実行すると次のようになります。
func test(int: Int, int2: Int, callback: (Int) -> Void) {
callback(int)
callback(int2)
}
// トレイリングクロージャを使用せずに実行
test(int: 1, int2: 2, callback: { x in print(x) })
トレイリングクロージャを使用すると、呼び出し元の {}
の処理を ()
の外に書くことができます。
func test(int: Int, int2: Int, callback: (Int) -> Void) {
callback(int)
callback(int2)
}
// トレイリングクロージャを使用して実行
// `callback: { x in print(x) }` を `()` の外に書く
test(int: 1, int2: 2) {
x in print(x)
}
また、引数がクロージャのみの場合は、呼び出し元の ()
を省略することができます。
// 引数がクロージャのみ
func test(callback: (Int) -> Void) {
callback(1)
}
// () を省略
test { int in
print(int) // 1
}
TIP
Swift UIなどで使われる HStack
や VStack
などもトレイリングクロージャを利用しています。
HStack {
VStack {
Text("1st")
Text("2st")
Text("3st")
}
}
実装を見ると、引数の最後にクロージャが定義されています。その他の引数はデフォルト値が設定されているので ()
を省略して書けるのです。
public struct HStack<Content> : View where Content : View {
/// Creates an instance with the given `spacing` and Y axis `alignment`.
///
/// - Parameters:
/// - alignment: the guide that will have the same horizontal screen
/// coordinate for all children.
/// - spacing: the distance between adjacent children, or nil if the
/// stack should choose a default distance for each pair of children.
@inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
}
SwiftUIの実装内部の詳しい説明は、DSLの章の SwiftUIを理解する をご確認ください。
# escaping
escapingとは、関数に引数として渡されたクロージャがスコープから抜けても存在しうる場合に付与する属性です。escapingをつけることによって、クロージャが関数内部の変数をキャプチャし、関数外のスコープでも変数を保持し実行することができます。コンパイラはescapingの有無でクロージャがキャプチャされる必要があるかを判断します。
次の例では、配列にクロージャを追加して、関数外のスコープでクロージャを実行しています。
引数の型の前に @escaping
属性を書きます。
var handlers: [() -> Void] = []
// 引数の型の前に @escaping 属性を書く
func addEscapingClosure(escapingHandler: @escaping () -> Void) {
handlers.append(escapingHandler)
}
addEscapingClosure {
print("Hello, 1")
}
addEscapingClosure {
print("Hello, 2")
}
// クロージャを関数のスコープ外から実行する
handlers.forEach { $0() }
// Hello, 1
// Hello, 2
もし、escapingをつけないとコンパイラはエラーを出力します。
var handlers: [() -> Void] = []
// @escaping をつけない
func addEscapingClosure(escapingHandler: () -> Void) {
handlers.append(escapingHandler)
}
addEscapingClosure {
print("Hello, 1")
}
addEscapingClosure {
print("Hello, 2")
}
handlers.forEach { $0() }
/* 実行結果 */
// passing non-escaping parameter 'escapingHandler' to function expecting an @escaping closure
// handlers.append(escapingHandler)
// ^
// note: parameter 'escapingHandler' is implicitly non-escaping
// func escapingClosure(escapingHandler: () -> Void) {
// ^
// @escaping
escapingをつけないと、クロージャは関数外のスコープで処理を実行できません。
関数内のスコープだけで実行するには次のように書く必要があります。
// 関数のスコープ内で実行する
func closure(handler: () -> Void) {
handler()
}
closure {
print("Hello, 1")
}
closure {
print("Hello, 2")
}
// Hello, 1
// Hello, 2
# escapingの用途
escapingは、クロージャがキャプチャされる場面で使用されます。具体的にはクロージャを非同期処理の後に実行したいときなどがあります。
# クロージャを非同期処理の後に実行したいとき
非同期処理をしたあとに、非同期処理完了後の処理としてクロージャを実行したい場合はescapingを使う必要があります。
通常、クロージャを関数の最後に同期的に実行したい場合は次のように書くことができます。
class A {
func syncFunc(completionHandler: () -> Void) {
print("syncFuncの処理が実行されました")
// クロージャを同期的に実行する
completionHandler()
}
}
let a = A()
a.syncFunc {
print("処理が完了しました!")
}
/* 実行結果 */
// syncFuncの処理が実行されました
// 処理が完了しました!
ここに、非同期の処理を追加してみましょう。
0.5
秒後に実行される asynFunc
メソッドを追加します。
import Foundation
class A {
func syncFunc(completionHandler: () -> Void) {
print("syncFuncの処理が実行されました")
completionHandler()
}
// 非同期処理の追加
func asynFunc(completionHandler: () -> Void) {
print("非同期処理が開始されました。")
// 0.5秒後に実行される
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
// クロージャを実行する
completionHandler()
}
}
}
let a = A()
a.asynFunc {
print("処理が完了しました!")
}
すると、コンパイラはエラーを出力します。
// closure use of non-escaping parameter 'completionHandler' may allow it to escape
// completionHandler()
// ^
// main.swift:9:19: note: parameter 'completionHandler' is implicitly non-escaping
// func asynFunc(completionHandler: () -> Void) {
completionHandler
が DispatchQueue.main.asyncAfter
のクロージャとして asynFunc
のスコープ外で実行されているためです。
スコープ外でも使用できるようにescapingを記述して、実行してみましょう。
import Foundation
class A {
func syncFunc(completionHandler: () -> Void) {
print("syncFuncの処理が実行されました")
completionHandler()
}
// @escaping をつける
func asynFunc(completionHandler: @escaping () -> Void) {
print("非同期処理が開始されました。")
// 0.5秒後に実行される
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
completionHandler()
}
}
}
let a = A()
a.asynFunc {
print("処理が完了しました!")
}
// 非同期処理が開始されました。
// 処理が完了しました!
非同期処理の後に、クロージャを実行することができました。
このように、escapingはクロージャをキャプチャして他の関数のスコープでも使えるようにできます。
# 循環参照 (強参照)
クロージャは、変数などをキャプチャして再利用可能な処理として定義されます。その際に、参照元をクロージャの中で参照すると、処理が終わった後でも参照し続けて循環参照になる可能性があります。これは、クロージャが参照元を強参照しているためです。
# 強参照されるパターン
強参照されるよくあるパターンが、クラスのselfプロパティをクロージャの中で強参照すると循環参照になることがあります。
通常、変数にnilが代入されるとメモリは解放されます。
class A {
deinit {
print("解放されました")
}
}
var a: A! = A()
a = nil
// 解放されました
しかし、クロージャがAのインスタンスを強参照している時は、nilが代入されてもメモリが解放されません。
class A {
var closure: (() -> Void)?
let prop = "prop"
// クロージャが、self.prop を強参照している
func setClosure() {
closure = {
print("\(self.prop) が呼ばれました")
}
}
deinit {
print("解放されました")
}
}
var a: A! = A()
a.setClosure()
a.closure!()
a = nil
// prop が呼ばれました
クラスのインスタンスとクロージャがお互いに参照しあっているため、循環参照が発生し、メモリの解放が行われないのです。
# weak / unowned (弱参照)
この循環参照を防ぐために、weakとunownedを使うことができます。
クロージャの引数の前に、[weak self]
と定義することでクロージャからselfの参照を弱参照にすることができます。
class A {
var closure: (() -> Void)?
let prop = "prop"
func setClosure() {
// `[weak self]` と定義して、selfへの参照を弱参照にする
closure = { [weak self] in
if let weakSelf = self {
print("\(self.prop) が呼ばれました")
}
}
}
deinit {
print("解放されました")
}
}
var a: A! = A()
a.setClosure()
a.closure!()
a = nil
/* 実行結果 */
// prop が呼ばれました
// 解放されました
weakは、変数がメモリから解放されるとnilになります。そのため、Optional型としてアンラップする必要があります。
unownedも同様の書き方はできますが、weakと違ってOptional型ではないので、変数がnilの場合は実行時エラーが発生します。
class A {
var closure: (() -> Void)?
let prop = "prop"
func setClosure() {
closure = { [unowned self] in
print("\(self.prop) が呼ばれました")
}
}
deinit {
print("解放されました")
}
}
var a: A! = A()
a.setClosure()
a.closure!()
a = nil
// prop が呼ばれました
// 解放されました
変数がnilだった場合は、実行時エラーが発生します。
class A {
var closure: (() -> Void)?
let prop = "prop"
func setClosure() {
closure = { [unowned self] in
print("\(self.prop) が呼ばれました")
}
}
deinit {
print("解放されました")
}
}
var a: A! = A()
a.setClosure()
// 変数にnilを入れあとに、クロージャを実行
let closure = a.closure
a = nil
closure!()
実行時のエラー
// Fatal error: Attempted to read an unowned reference but object 0x55e602bb31a0 was already // deallocatedCurrent
unownedを使う場合は、実行時に変数がnilにならない条件で使うのがいいでしょう。
TIP
循環参照については、メモリ管理 の章でも詳しく説明しているのでご確認ください。
# autoclosure
autoclosure
は、引数をクロージャでラップすることによって受け取った処理を遅延、またはスキップすることができます。
# 遅延評価とは
autoclosureの説明の前に遅延評価について見てみましょう。
遅延評価とは、簡単にいうと、値が必要になるまでその値の評価を遅らせるということです。
次の例を見てみましょう。
func func1() -> Bool {
print("func1が実行されました")
return true
}
func func2() -> Bool {
print("func2が実行されました")
return true
}
func printFunc(func1: Bool, func2: Bool) {
if (func1) {
print("func1はtrueです")
} else if (func2) {
print("func2はtrueです")
} else {
print("どちらともfalseです")
}
}
printFunc(func1: func1(), func2: func2())
// func1が実行されました
// func2が実行されました
// func1はtrueです
printFunc
は、引数を2つとり、それぞれの結果を出力しています。
この処理の順番を時系列に見ると、次のようになります。
func1()
が実行func2()
が実行printFunc
が1と2の結果を引数として受け取り、評価する
printFunc
の実装を見ると、func1
の条件式が true として評価されて処理が実行され、func2
の条件式の処理は実行されません。この場合、2. func2() が実行
の部分が関数として実行されているにも関わらず、無駄な評価になってしまいます。
この無駄な処理を防ぐために、クロージャを使用して、func1
が true のときは func2()
の実行を遅らせてみましょう。
func func1() -> Bool {
print("func1が実行されました")
return true
}
func func2() -> Bool {
print("func2が実行されました")
return true
}
// func2をクロージャ型にする
func printFunc(func1: Bool, func2: () -> Bool) {
if func1 {
print("func1はtrueです")
} else if func2() {
print("func2はtrueです")
} else {
print("どちらともfalseです")
}
}
// クロージャを引数として渡す
printFunc(func1: func1(), func2: { func2() })
出力結果で、func2()
が実行されていないことがわかります。
// func1が実行されました
// func1はtrueです
このように、値をクロージャでラップして実行を後回しにさせることで評価を遅らせることができました。この値の評価のタイミングを遅らせることを遅延評価といいます。
# autoclosureで遅延評価する
Swiftでは、autoclosure
を使うことによって、呼び出し元でクロージャを渡さなくても値を遅延評価してくれます。
autoclosureを利用するには、@autoclosure
を引数に記述してください。
次の例では、引数の func2
をautoclosureとして定義しています。
func func1() -> Bool {
print("func1が実行されました")
return true
}
func func2() -> Bool {
print("func2が実行されました")
return true
}
// func2 に @autoclosure を定義する
func printFunc(func1: Bool, func2: @autoclosure () -> Bool) {
if func1 {
print("func1はtrueです")
} else if func2() {
print("func2はtrueです")
} else {
print("どちらともfalseです")
}
}
// func2 に値を引数として渡す
printFunc(func1: func1(), func2: func2())
実行結果を見ると、func2()
が実行されていないことがわかります。
// func1が実行されました
// func1はtrueです
このようにautoclosureを使うと、呼び出し元が明示的にクロージャを渡さなくても呼び出し先が暗黙的にクロージャとして値をラップして、遅延評価してくれるようになります。
通常だと、呼び出し元がクロージャを渡さないといけないという煩雑さをautoclosureを使うことによって、回避することができます。
ただ、シンプルに遅延評価を実装することができますが、多用すると可読性が落ちるという側面もあるので注意しましょう。
# クロージャ式の用途
# mapメソッド
配列を操作するmapメソッドには、関数を渡すこともできますが、クロージャ式を渡すこともできます。
次の例で、配列の値を2倍にする処理を書いてみましょう。
let numbers = [1,2,3,4,5]
let mapped = numbers.map({ (num: Int) -> Int in
return num * 2
})
上記で説明した通り、型やreturn文は省略して書くことができます。ひとつずつ見てみましょう。
# 型を省略する
let numbers = [1,2,3,4,5]
// 元の処理
// let mapped = numbers.map({ (num: Int) -> Int in
// return num * 2
// })
// 型を省略する
let mapped = numbers.map({ num in
return num * 2
})
mapped // [2, 4, 6, 8, 10]
# return文を省略する
let numbers = [1,2,3,4,5]
// 元の処理
// let mapped = numbers.map({ (num: Int) -> Int in
// return num * 2
// })
// return文を省略する
let mapped = numbers.map({ num in
num * 2
})
mapped // [2, 4, 6, 8, 10]
# 引数名を省略する
let numbers = [1,2,3,4,5]
// 元の処理
// let mapped = numbers.map({ (num: Int) -> Int in
// return num * 2
// })
// 引数名を省略する
let mapped = numbers.map({
$0 * 2
})
mapped // [2, 4, 6, 8, 10]
# トレイリングクロージャを使う
let numbers = [1,2,3,4,5]
// 元の処理
// let mapped = numbers.map({ (num: Int) -> Int in
// return num * 2
// })
// トレイリングクロージャを使う
let mapped = numbers.map { $0 * 2 }
mapped // [2, 4, 6, 8, 10]
このように省略することによって、簡潔に書くこともできます。