クロージャについて段階を経て理解する

6 min read読了の目安(約5500字

はじめに

私がクロージャを理解するために段階を経て学んでいる様子を記事にしました。これまでクロージャについてはコピペでなんとかしてきたのですが、向き合う機会ができたので学んでいます。

段階その1 型の種類

Swiftの型には整数型や文字列型の他に(Int) -> Intといった引数と戻り値をセットにした型を定義することができる。

let plus: (Int,Int) -> Int

plusは二つのIntを受け取ってIntを返す(Int,Int) -> Int型。

plus = { (x: Int, y: Int) -> Int in
           return x + y
}

ここでplusに処理を加え,クロージャとして定義することができた。

plus(10,5)//15

挙動はこのようになる。

段階その2 returnの省略

値の返却しか行わない場合はreturnを省略できる。

上述したコードにおいても

plus = { (x: Int, y: Int) in
     x + y
}

のように記述することができる。

段階その3 引数の型の省略

型をあらかじめ宣言しておいた場合、引数の型の定義を省略することができる。

let plus: (Int, Int) -> Int
plus = { x,y in
     x + y
}
plus(10,2)//12

1行目に書かれていることは段階その2までで行っていたことと変わっていない。

2行目に書かれている内容はx: Int, y: Intx,yのみになっている。これは1行目で型を定義しておいたことによる恩恵のようなものである。

段階その4 引数としてのクロージャ

関数の引数に関数を用いたい時に、クロージャが活躍するように思う。

例えば複数の整数が格納された配列numArrに対し、それらがそれぞれ素数であるのか判定したり,

それらの二乗を求めたかったりしたいとしよう。この時、配列と関数を引数に持つ関数を考えることができる。

そのような抽象的な関数を定義し、要素が全て素数であるのか判定したい時はその処理を引数にすれば良い。

func numArrAndClosure(numArr: [Int], closure: (Int) -> Void) {
    for num in numArr {
        closure(num)
    }
}

numArrAndClosure(numArr: [1,2,3,4,5], closure: { x in print(x+1) })

上のコードではnumArrAndClosureの中で配列の要素全てに、引数のclosureを行なうよう定義している。

closure行いたい処理を入れれば良いので、別の場所で定義をしよう。

extension Int {
    var isPrimeNumber: Bool {
        if self == 1 {
            return false
        }
        if self == 2 || self == 3{
            return true
        }
        
        for i in 2 ... Int(sqrt(Double(self))) {
            if self%i == 0 {
                return false
            }
        }
        return true
    }
}

let primeJudge: (Int) -> Void
primeJudge = { x in
    print(x.isPrimeNumber)
}

primeJudge(Int) -> Voidで、素数か判定するクロージャである。

これをnumArrAndClosureの引数にすることで要素の全てが素数であるのか判定することができるようになった。

numArrAndClosure(numArr: [1,2,3,4,5], closure: { x in primeJudge(x) })

二乗を行いたい場合についても、以下のように行えば良い。

numArrAndClosure(numArr: [1,2,3,4,5], closure: {x in print(x*x) })

段階その5 トレイリングクロージャ

段階4で行ったクロージャを引数にもつ関数は、トレイリングクロージャ と呼ばれる記法で書き直すことができる。

numArrAndClosure(numArr: [1,2,3,4,5], closure: { x in primeJudge(x) })

numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in primeJudge(x)
}

上の二つのコードはそれぞれ同じ挙動をする。後者の方が可読性が高いので、トレイリングクロージャ が推奨されているのではないかと感じる。

段階その6 @escaping

ここから@capingの説明にはいる。ここからが関門であるかと思う。

numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in primeJudge(x)
}

先ほどのこのx in primeJudge(x)numArrAndClosureのスコープ内でしか行うことができない。これをスコープ外で行うために用いるのが@escapingである。

実務の中で必要に応じて使わない限り、その必要性について感じることはできないと思う。ここでは

配列の全ての要素を2乗する関数,3乗する関数,4乗する関数,5乗する関数をそれぞれ任意の順番で行うことを目的にしようと思う。

段階4で関数を引数に持つ関数について記述した。それを応用する形になる。

extension Int {
    var isPrimeNumber: Bool {
        if self == 1 {
            return false
        }
        if self == 2 || self == 3{
            return true
        }
        
        for i in 2 ... Int(sqrt(Double(self))) {
            if self%i == 0 {
                return false
            }
        }
        return true
    }
    func pow(toPower: Int) -> Int {
            guard toPower > 0 else { return 0 }
            return Array(repeating: self, count: toPower).reduce(1, *)
        }
    
}

まずは準備として,4.pow(2)//16となるようにIntを拡張した。(この記事の本筋ではないので,説明しない)

これで,numArrAndClosureの引数のクロージャに、各要素を2乗、3乗...するように処理を書けば良いので

numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in print(x.pow(toPower: 2))
}
numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in print(x.pow(toPower: 3))
}
numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in print(x.pow(toPower: 4))
}
numArrAndClosure(numArr: [1,2,3,4,5]) {
    x in print(x.pow(toPower: 5))
}
1
4
9
16
25
1
8
27
64
125
1
16
81
256
625
1
32
243
1024
3125

のようになる。

これでは記述した順番に処理が行われてしまう。4つの関数を任意の順番で行うようにするためには@escapingが必要になる。

処理だけを配列に格納し、後からその配列を呼び出すといった流れだ。

var handlers: [([Int]) -> Void] = []
func addHandlers(escapingHandler: @escaping ([Int]) -> Void) {
    handlers.append(escapingHandler)
}

hanlersは配列を引数に持ち、処理を行う関数を格納することができる配列である。そしてaddHandlersはその[Int]) -> Voidを引数に持ち、handkersに処理を格納する。

var handlers: [([Int]) -> Void] = []

func addHandlers(escapingHandler: @escaping ([Int]) -> Void) {
    handlers.append(escapingHandler)
}

addHandlers{arr in
    numArrAndClosure(numArr: arr) {
        x in print(x.pow(toPower: 2))
    }
}
addHandlers{arr in
    numArrAndClosure(numArr: arr) {
        x in print(x.pow(toPower: 3))
    }
}
addHandlers{arr in
    numArrAndClosure(numArr: arr) {
        x in print(x.pow(toPower: 4))
    }
}
addHandlers{arr in
    numArrAndClosure(numArr: arr) {
        x in print(x.pow(toPower: 5))
    }
}

handlers[1]([1,2,3,4,5])
handlers[0]([1,2,3,4,5])
handlers[2]([1,2,3,4,5])
handlers[3]([1,2,3,4,5])

そして、トレイリングクロージャ記法を用いて、処理を格納し、最後に任意の順番で呼び出してる。

@escapingを用いることで、関数の引数に用いられたクロージャを、関数の外側でも用いることができるようになった。この恩恵は非同期処理のように主に処理の順番を変えたい時に受けることが多いらしい。

段階その7 非同期処理

@escapingを用いて、非同期処理を行う。

// 非同期処理の追加
func asynFunc(completionHandler:@escaping () -> Void) {
      print("処理1")
      // 0.5秒後に実行される
      DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
        // クロージャを実行する
        completionHandler()
      }
    print("処理3")
}

asynFunc {
    print("処理2")
}
//処理1
処理3
処理2

処理1 -> 処理2 -> 処理3とデバックされていることが確認される。
また、@escapingは非同期処理に実装時以外に強参照される時にもつけるようようだ。
それらの具体的な実装についてはまた別の機会にしようと思う。

参考記事

http://www.kuma-de.com/blog/2017-10-18/7381

https://swift.codelly.dev/guide/