【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でも同様です。

関数の定義の型としてクロージャを指定しておきます。あとは呼び出し時に型にあったクロージャ(または関数)を渡すだけです。

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が必要になります。

参照の強さはweakunownedキーワードを使用して指定します。

{ () -> 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)さん")
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index