はじめに
Swiftを使って趣味でアプリ開発をしている者です。
自分の中の理解を深めるために、Swiftのクロージャについてまとめてみました📝
ここでは、
- クロージャというものを初めて勉強する
- クロージャについて調べたけど、イマイチわからなかった
といった方に向けて記事を書いてみようと思います。
サンプルコードについて
Swift 5.8を前提として書いています。
まだ動作未確認ですが、更新内容を見る限りSwift 5.9でも大きく変わることはなさそうです。
関数型
クロージャの説明をするために、まずは関数型というものについて説明します。「関数型?そんなの知ってるよ〜」という方は読み飛ばしていただいて大丈夫です。
全ての関数には型があり、 関数型(Function Types) というものになります。以下の関数の型を見てみましょう。
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiply(_ a: Int, _ b: Int) -> Int {
return a * b
}
func printHelloWorld() {
print("Hello world!")
}
add 関数、multiply関数の型は (Int, Int) -> Int 、 printHelloWorld 関数の型は () -> Void となります。関数の()から後ろを抜き出したようなイメージですね。
関数型の変数・定数
関数の名前のみを書くことで、関数を変数や定数に代入したり、代入した関数を呼び出すことができます。
// 変数に代入する
var functionInVariable: (Int, Int) -> Int = add
// 定数に代入する
let functionInConstant: () -> Void = printHelloWorld
もちろん他の型と同じように、変数や定数の型は省略できます。
var anotherFunction = multiply
代入した変数・定数の名前を使って元の関数を呼び出してみましょう。
// add関数を変数から呼び出す
let mathResult: Int = functionInVariable(2, 3)
print(mathResult) // 5
// printHelloWorld関数を定数から呼び出す
functionInConstant() // Hello world!
このように、代入した変数・定数の名前に元の関数のパラメータを続けることで、元の関数を呼び出すことができます。
関数型をパラメータにとる
パラメータの型を関数型にして、関数型をパラメータにとる関数を定義してみました。
func calculateTwoAndThree(with function: (Int, Int) -> Int) -> Int {
return function(2, 3)
}
calculateTwoAndThree関数は2と3という整数をwith functionパラメータに渡された関数で計算して、結果を戻り値として返します。
パラメータの型は(Int, Int) -> Int型なので、実際に例1−1のadd関数を渡してみましょう。
let addedResult = calculateTwoAndThree(with: add)
print(addedResult) // 5
with functionパラメータに渡されたadd関数で2 + 3が計算されました。
let multipliedResult = calculateTwoAndThree(with: multiply)
print(multipliedResult) // 6
2 * 3が計算されましたね!
関数型を戻り値として返す
次は関数型を戻り値として返す関数を考えてみましょう。
func printHelloWorld() {
print("Hello world!")
}
func printHello() {
print("Hello!")
}
// 戻り値は () -> Void
func makeGreeter(toWorld: Bool) -> () -> Void {
if toWorld == true {
return printHelloWorld
} else {
return printHello
}
}
makeGreeter関数を定義しました。toWorldパラメータがtrueの時にprintHelloWorld関数、falseの時にprintHello関数が戻り値として返されます。
makeGreeter関数はあくまで関数型を戻り値として返します。ただ単にmakeGreeter関数を呼び出しても、文字列は何も出力されません。
定数に代入して、戻り値の関数を呼び出してみましょう。
let sayHello: () -> Void = makeGreeter(toWorld: false)
sayHello() // Hello!
let sayHelloToWorld: () -> Void = makeGreeter(toWorld: true)
sayHelloToWorld() // Hello world!
定数に代入された関数がちゃんと動作しました!
ネスト関数
さて、例1-7で示したコードではmakeGreeter関数の外側にprintHello関数とprintHelloWorld関数を定義していますが、次のように関数の中に関数を定義することもできます。これを ネスト関数(Nested Functions) といいます。
func makeGreeter(toWorld: Bool) -> () -> Void {
// ネスト関数を定義
func printHelloWorld() -> Void { print("Hello world!") }
func printHello() -> Void { print("Hello!") }
if toWorld == true {
return printHelloWorld
} else {
return printHello
}
}
例1-7のコードよりスッキリしましたね!
クロージャ式
ここまで関数型を扱うには、いちいち関数に名前をつけて定義する必要がありました。パラメータに関数型を渡すときにいちいち関数を定義していては骨が折れます。そこで クロージャ式(Closure Expressions) の出番です。
{ (パラメータ: パラメータの型) -> 戻り値の型 in
処理の内容
}
実際に使ってみます。例1-1、1-2でadd関数をfunctionInVariable変数に代入しましたが、同じ処理をクロージャ式で書いてみましょう。
// クロージャ式を使って関数型を変数に代入
var functionInVariable: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
print(functionInVariable(2, 3)) // 5
=より後ろの部分がクロージャ式です。例1-1、1−2ではいったんadd関数を定義してからfunctionInVariable変数に代入しましたが、ここではクロージャ式を使って、直接関数型をfunctionInVariable変数に代入しています。
この場合、変数の型もしくはクロージャ式の型のどちらかを省略できます。
// 変数側の型を省略
var functionInVariable1 = { (a: Int, b: Int) -> Int in
return a + b
}
// クロージャ式側の型を省略
var functionInVariable2: (Int, Int) -> Int = { a, b in
return a + b
}
関数と同じように、処理が1行のみの時にはreturnを省略することができます。このことを使って、これまでの例を1行で書いてみました。
var functionInVariable3: (Int, Int) -> Int = { a, b in a + b }
さらには、パラメータ部分を省略することもできます。その場合、パラメータは順に$0, $1, $2……と割り振られます。
var functionInVariable4: (Int, Int) -> Int = { $0 + $1 }
この書き方の場合は可読性が下がるので、パラメータが1個だけの時など、パラメータが何なのか明らかな時にだけ使うことをオススメします。
末尾クロージャ
クロージャ式は関数のパラメータに関数型を渡す時によく使われます。
例1−5のcalculateTwoAndThree関数にクロージャ式を渡してみましょう。
let multipliedResult = calculateTwoAndThree(with: { (two: Int, three: Int) -> Int in
two * three
})
1行が少々長くなってしまったので、クロージャ式の型を省略して推論してもらいます。
let multipliedResult = calculateTwoAndThree(with: { two, three in
two * three
})
print(multipliedResult) // 6
多少はマシになりましたが、それでもパラメータの丸括弧が複数行にわたって続いていて読みづらいと思います。クロージャ式の処理が複数行あるなら尚更です。
そこで 末尾クロージャ (Trailing Closures) という記法を使います。この記法を使うと、パラメータに渡すクロージャ式の波括弧を丸括弧の外側に出すことができます。
let multipliedResult = calculateTwoAndThree() { two, three in
two * three
}
さらに、末尾クロージャの他にパラメータがない場合は、パラメータの丸括弧すら省略できてしまいます!
let multipliedResult = calculateTwoAndThree { two, three in
two * three
}
クロージャ
ここまで見てきたような、コードで受け渡して使用できる、機能の独立したブロックを クロージャ (Closure) といいます。クロージャ式だけではなく、グローバル関数やネスト関数も含めてそう呼ばれます。
値のキャプチャ
クロージャは変数や定数の参照を キャプチャ (capture) して保持できます。どういうことなのか見てみましょう。
func makeCounter() -> () -> Void {
var totalCount = 0
func counter() {
totalCount += 1
print(totalCount)
}
return counter
}
var count: () -> Void = makeCounter()
makeCounter関数の中にはtotalCount変数が定義されていて、この関数はtotalCountに1を足した上で出力するcounterネスト関数をクロージャとして返します。
さて、counter関数が代入されたcount変数はmakeCounter関数のスコープ外にありますが、スコープ内にあるtotalCount変数を呼び出せるのでしょうか……?
count() // 1
count() // 2
count() // 3
totalCount変数を出力する上に、足した値も保持されています。クロージャが定数や変数の参照を閉じ込める(close over)ので、スコープ外でもtotalCount変数にアクセスできるんですね。
まとめ
- 関数の型を 関数型 といい、
(パラメータの型) -> 戻り値の型と表される。- 関数型は変数に代入したり、関数のパラメータとして渡したり、戻り値として返すことができる。
- 関数の中に関数を定義することができ、ネスト関数 という。
-
クロージャ式 を使って、簡単に関数型を記述できる。
- クロージャ式は関数のパラメータに関数型を渡すのによく使われる。
- 関数のパラメータにクロージャ式を渡すとき、末尾クロージャ を使って簡潔に書ける。
- グローバル関数、ネスト関数、クロージャ式を合わせて クロージャ と呼ばれる。
- クロージャは変数や定数の参照を キャプチャ できる。
さいごに
実はQiitaに初めて投稿するので、文章の読みづらい点があるかもしれません。そういった点や何か間違っている点などがありましたら、コメントにてご指摘をお願いします🙇
参考資料
関数(Functions) · The Swift Programming Language日本語版
https://www.swiftlangjp.com/language-guide/functions.html
クロージャ(Closures) · The Swift Programming Language日本語版
https://www.swiftlangjp.com/language-guide/closures.html
Closures | Documentation
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures
Swift.org - API Design Guidelines
https://www.swift.org/documentation/api-design-guidelines/
こちらはサンプルコードの変数・関数名を考える上で参考にしました。読みやすいSwiftコードを書くうえで大事なことがたくさん書いてあります。オススメです。