【Swift】クロージャとは?関数との違いとキャプチャの意味
この記事からわかること
- Swiftのクロージャとは?
- クロージャと関数の違い
- 使い方とメリット
- キャプチャとは?
- キャプチャリストの使用方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
クロージャ(Closure)とは?
Swiftのクロージャ(Closure)とはまとまった処理を定義できるデータ構造のことです。再利用可能で引数に渡したり、型として定義することもできます。
関数はfunc
キーワードを使って宣言するという特徴はありますが、クロージャの1種になります。
クロージャはクロージャ式に倣って記述します。クロージャ式は名前が必要なく、省略可能な部分も多いため、可読性や軽量化の観点からも汎用性が高いデータ構造です。
「無名関数」や「関数閉包」と呼ばれることもあるようです。
クロージャの種類
関数もクロージャの1種ということは他にも種類があるはずです。クロージャの種類を見てみます。
- グローバル関数:名前あり、キャプチャ無しのクロージャ
- ネストされた関数:名前あり、親の関数の値のみキャプチャ可能のクロージャ
- 無名関数:名前無し、キャプチャ可能のクロージャ
こちらの記事を参考にさせていただきました。
メリット
クロージャ(無名関数)のメリットを挙げてみます。
- 記述を省略できる
- 処理をまとめて定義できる
- 型として活用できる
- 変数や定数のキャプチャ
大きなメリットとしては記述が大幅に省略できることで可読性がグンと向上することです。省略する条件やキャプチャについては後述しています。
クロージャの基本構文
クロージャ式は波かっこ{}
で囲った中に処理を記述します。中に記述した処理を1つの単位として扱うことができるようになり、変数に格納したり、関数などの引数に渡すことも可能です。
{ (引数) -> 戻り値の型 in
処理
return
}
省略できる記述方法
クロージャの構文は状況によって省略することができます。
返り値があるが1行のみの場合
クロージャ内に記述するステートメントが1行の場合、return
を省略することができます。これはImplicit Returns(暗黙的なreturn)と呼ばれています。
let closure = { () -> Int in
2 * 5
}
引数がない場合
引数がない場合は()
のみでOKです。また返り値が無い時はVoid
型を明示的に指定します。
let closure = { () -> Void in
print("foo")
}
返り値もない場合
返り値が無い場合わざわざVoid型でなくとも()
だけの記述でも可能です。これは()
とVoid
は同等の意味を持っているからです。
let closure = { () -> () in
print("foo")
}
さらに() -> () in
も省略することができます。
let closure = { print("foo") }
使い方
クロージャを変数に格納する
クロージャは変数に格納することもできます。変数に格納したクロージャは変数名()
の形式で実行することができます。
let closure = { () -> () in
print("クロージャのテスト")
}
closure() // クロージャのテスト
型として使用する
クロージャは変数を定義する際の型として扱うことも可能です。型として指定するには通常通り:
の後にクロージャを指定します。その際は{}
とin
の記述は不要です。
let closure:(Int,Int) -> String
closure = { (num1: Int, num2: Int) -> String in
return String(num1 + num2)
}
closure(10,20) // "30"
クロージャの型を指定した変数には同じ型のクロージャもしくは関数を格納することができます。
func changeNum(num1:Int,num2:Int) -> String{
return String(num1 + num2)
}
let closure:(Int,Int) -> String
closure = changeNum
closure(10,20) // "30"
関数の引数としてクロージャを渡す
クロージャは関数の引数として渡すことも可能です。このように関数の中で実行される関数(クロージャ)のこと「コールバック関数」と呼んだりします。 これはjavascriptでも同様です。
おすすめ記事:【javascript】コールバック関数とは?
関数の定義の型としてクロージャを指定しておきます。あとは呼び出し時に型にあったクロージャ(または関数)を渡すだけです。
func sample(num1:Int,cl:(Int) -> String){
if num1 >= 5 {
print("これは5以上の数字\(cl(num1))です")
}
}
func changeNum(num1:Int) -> String{
return String(num1)
}
// 関数の呼び出し
sample(num1: 7, cl:changeNum) // これは5以上の数字7です
キャプチャとは?
クロージャのメリットの1つが変数や定数などをキャプチャできることです。キャプチャとはスコープ外の変数などをクロージャ内から操作、参照できる仕様のことです。
通常関数内で定義された変数はその関数外からは参照することができません。これはその変数のスコープが関数内に制限されており、関数の中で使用済みで不要になった変数は自動で破棄されるからです(今回の場合localCount)。
func sample() -> Int{
var localCount = 0
return localCount
}
print(localCount)
// エラー:Cannot find 'localCount' in scope
しかしクロージャでは関数内に定義した変数を外部から参照、操作ができるようになっています。これはネストされた関数でみると挙動が分かりやすいです。
func sample2() -> () -> Int{
var localCount:Int = 0
let closure = { () -> Int in
localCount = localCount + 1
return localCount
}
return closure
}
var cl = sample2() // クロージャを変数に格納
dump(cl) // () -> Int
cl() // 1 問題なく実行できる
cl() // 2 さらにlocalCountの値が保持され
cl() // 3 実行のたびにインクリメントされていく
sample2関数の返り値はクロージャにしてあります。これにより中に定義したクロージャをそのまま外部へ持ち出すことができます。つまり変数cl
に格納されているのは{ () -> Int in localCount = localCount + 1 return localCount }
ということになります。dump
でみてみるとクロージャであることがわかります。
クロージャの中ではlocalCount
は未定義のはずですが、問題なく実行することができ、さらにクロージャの実行のたびにインクリメントされていきます。つまりlocalCount
は自動で破棄されることなく、メモリに残ったままになるのです。
ポイント
- 関数内で定義された変数には外部からアクセスできない
- 関数内の変数は実行後破棄される
- クロージャは参照している変数が関数内のものでも外部で実行できる
- クロージャで参照している変数は破棄されない
キャプチャリスト
クロージャでキャプチャした変数や定義はメモリと強い参照(強参照)で紐付けられます。そのため循環参照を引き起こす可能性を孕んでおりメモリが解放されなくなる恐れがあります。強参照や弱参照、循環参照については以下記事をご覧ください。
おすすめ記事:【Swift】weakの役割とは?循環参照の意味とARCについて
これを防ぐためにはキャプチャリストを使用して明示的に変数の参照を制御することができます。
記述方法は[変数名]
形式で引数の前に記述します。クロージャの省略記法を使用している場合でもキャプチャリストを記述する場合はin
が必要になります。
参照の強さはweak
やunowned
キーワードを使用して指定します。
{ () -> Void in print("foo") } // 暗黙的な強参照
{ print("foo") } // 暗黙的な強参照
{ [self] in print("foo") } // 明示的な強参照
{ [weak self] in print("foo") } // 明示的な弱参照
{ [unowned self] in print("foo") } // 明示的な非所有参照
関数とクロージャの違い
関数(グローバル関数)とクロージャ(無名関数)は同じ種類に属しているとはいえ仕様や挙動には違いがあります。
両者の明確な違い
- 名前があるかないか
- 値をキャプチャできるかできないか
- 外部引数名の使用の是非
- デフォルト引数の使用の是非
外部引数名の使用の是非
クロージャでは外部引数名が使用できません。外部引数名とは引数指定時に外部から渡す時の引数名と内部から扱う引数名を別々にするために使われます。
// クロージャはエラー
// エラー:Closure cannot have keyword arguments
var closure = { (customer user : String) -> Void in
print("こんにちは\(user)さん")
}
// 関数ならOK
func printHello (customer user : String) {
print("こんにちは\(user)さん")
}
デフォルト引数の使用の是非
同様にデフォルト引数も使用することができません。デフォルト引数は引数に対してデフォルト値を設定することです。
// クロージャはエラー
// エラー:Default arguments are not allowed in closures
var closure = { (user : String = "ame") -> Void in
print("こんにちは\(user)さん")
}
// 関数はOK
func printHello (user : String = "ame") {
print("こんにちは\(user)さん")
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。