[Swift]クロージャーの使い方

Swiftの文法で特に分かりづらいものの1つクロージャーの使い方を見ていきます。

環境

今回の記事は以下の環境で実行することを想定している記事です。

  • Swift2.2
  • Xcode7.3.1

クロージャー

クロージャーは自分を囲むスコープにある変数を参照する関数のことをいいます。(変数をキャプチャするといいます。)
Swiftは第一級関数をもつ言語で、関数そのものを型に指定できたり、関数をインスタンス化できます。
関数をインスタンス化したら、それはクロージャーになると考えて大丈夫です。
関数に名前がない無名関数としても定義することができます。

【なぜクロージャ(Closure)と言うのか?】
http://qiita.com/mochizukikotaro/items/7403835a0dbb00ea71ae

各言語、クロージャーの概念はなかなかとっつきにくいものがあります。上記のqiitaの記事は言語はJavaScriptですが、クロージャーが何なのかを理解する助けになると思います。


Swiftのクロージャーの特徴

Swift特有のクロージャーの特徴は以下です。

  • 文脈から引数と返り値の型を推定する
  • 暗黙の返り値を返す
  • 引数の変数を略式でかける
  • Trailing closure
    • 関数の引数の末尾(最後)がclosureを受け取る場合、処理部分を外に配置できる

どんどんシンタックスシュガーとして略して書くことがSwiftのクロージャーではできるので、コードを読む場合にびっくりしないように気をつけましょう。


クロージャー文法

クロージャーを作るには以下のような文法で作ります。

 { (引数) -> 戻り値の型 in 処理
 }

次からは色々なクロージャーを作って実行してみます。

クロージャーの例

引数、戻り値のないクロージャは以下のように作ります。

//クロージャとして、関数をインスタンス化できる
//引数、戻り値のないクロージャ
var c1 = { () -> () in print("Hello") }    
c1()

Swiftのクロージャーは引数と返り値の型は推論が聞くので分かりきっている場合は省略が可能です。

//省略なし
var c1 = { () -> () in print("Hello") }
c1()

//戻り値を()ではなくVoidでかく
var c2 = { () -> Void in print("Hello") }
c2()

//式がprint文で戻り値がないことがわかっているので戻り値を省略
var c3 = { () in print("Hello") }
c3()

//引数もないので省略
var c4 = { print("Hello") }
c4()
    

c2は戻り値を()ではなくVoidで記述しています。
c3は処理文のprint("Hello")が戻り値のない関数なので戻り値->()を省略して記述しています。
c4では引数もないのでそれも省略しています。


クロージャの型宣言

クロージャーも関数とはいえ、オプショナル値で型を宣言することができます。

//「引数Int,IntでDouble型を返す」クロージャのオプショナル型
var cp2 : ((Int, Int) -> Double)?

クロージャーもインスタンス化すれば配列に入れることができます。
ただし、シンタックスシュガーでの定義だと関数定義そのままではエラーになります。
typealiasで別名をつけるとうまくいきます。

//クロージャを配列にいれるときは、typealiasにするとよい
//そのままではエラーになる
//var ca3 = [(Int, Int) -> Double]()
    
//typealiasにすると配列に格納できる
typealias MyClosure = (Int, Int) -> Double
var ca4 = [MyClosure]()

ca3(Int, Int) -> DoubleというInt型を2つ引数にもちDouble型を返す関数を配列に入れようとしていますが、そのままではエラーになります。
typealias MyClosure = (Int, Int) -> DoubleとしてMyClosureという別名をtypealiasでつけて改めてvar ca4 = [MyClosure]()で入れると問題なくクロージャーを配列に入れることができます。

配列をシンタックスシュガーではなく、Array<型>()のように初期化すればそのままでも定義ができます。

//シンタックスシュガーでないなら入れられる
var ca5 = Array<(Int, Int) -> Double>()

変数のキャプチャ

クロージャはインスタンスが作らるときにクロージャの外側にある変数を取り込んでインスタンスの一部にして、インスタンスが呼び出されるときにその値を使うことができます。これをキャプチャと呼びます。

ネスト関数を使ってこれを見てみます。

引数がなく、戻り値に引数なしで戻り値もないクロージャーを返すouter関数を定義します。

func outer() -> () -> Void{
    
    var x = 10
    
    func inner(){
        
        print("X is \(x)")
        x  = x + 1//inner関数で定義されていないx変数を操作している。
    }
    return inner//関数を戻り値として返している
}

//outer関数を実行
var f = outer()

//実行する
f()//-> X is 10
f()//-> X is 11
f()//-> X is 12
//->キャプチャされたx変数がインクリメントされた。

outer関数にネストする形でinner関数を定義しています。
inner関数は自身では定義していない(outer関数ブロック内で定義された)xという変数を1プラスしています。
そしてouter関数はinner関数を戻り値として返します。  
var f = outer()でouter関数を実行するとinner関数のインスタンスが戻り値として返ってきます。
これを実行すると、X is 10X is 11と、数値がプラス1ずつ大きくなります。
inner関数でx変数がキャプチャされて、それがインクリメントされているからです。


sort(_:)メソッドでクロージャーの書き方を見てみる

Array型にはsort(_:)という、各要素をソートするメソッドがあります。
このメソッドをもとにSwiftのクロージャーはどんな書き方ができるのかを見てみます。

まず、ソートする配列を定義してみます。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

関数を定義してみる

sort(_:)メソッドはArray型の要素の型の2つ引数とBool型の戻り値の関数を引数に持ちます。

なので、引数にString型を2つもち、Bool値を返す関数backwardsを定義してみます。

func backwards(s1: String, _ s2: String) -> Bool{
    return s1 > s2
}

var reversed = names.sort(backwards)

print(reversed)
//->["Ewa", "Daniella", "Chris", "Barry", "Alex"]

backwards関数はreturn s1 > s2としてします。こうするとsort(_:)メソッド入れた場合に配列要素がアルファベット大きい順番に並ぶようになります。
var reversed = names.sort(backwards)backwards関数をsort(_:)メソッドの引数に入れます。
戻り値のreversed配列をみてみると["Ewa", "Daniella", "Chris", "Barry", "Alex"]とアルファベットの大きい順番に並び替えられているのがわかると思います。

無名関数で実行

いちいちbackwardsのような関数を定義するのも煩わしい場合があります。
先ほどと同じ結果になるソートを無名関数のクロージャーで実行してみます。

/*
 無名関数で実行してみる
 */
let reversed = names.sort({ (s1:String, s2: String) -> Bool in
    return s1 > s2
})

print(reversed)
//->["Ewa", "Daniella", "Chris", "Barry", "Alex"]

sort(_:)メソッドの引数に以下のようなクロージャーが入っているのが見えるでしょうか。

{ (s1:String, s2: String) -> Bool in
    return s1 > s2
}

引数、戻り値の省略

ここからどんどん省略していきます。

Swiftでは型を推定することができます。Swiftの配列は要素の型が配列がインスタンスされるさいに決定されます。なので配列の要素の方は指定しなくても推定できます。
今回のnames配列は要素の型はStringです。
推定できるから(String, String) -> Boolの記述は省略できます。

/*
 Swiftは型を推定。
 配列の型は推定できるから(String, String) -> Boolの記述は省略できる
 */
let reversed = names.sort({
    s1, s2 in return s1 > s2
})

print(reversed)
//->["Ewa", "Daniella", "Chris", "Barry", "Alex"]

処理の部分、{s1, s2 in return s1 > s2}でいきなりs1,s2と書いてもSwiftが推論して型をString型にしています。またs1 > s2と比較することで戻り値がBool値になっているのでわざわざクロージャーに戻り値を宣言しなくてもよくなっています。

単式での戻り値の省略

処理を単式で書いた場合は戻り値は暗黙に返されます。なので'return'の文字は省略できます。

let reversed = names.sort({s1, s2 in s1 > s2})
print(reversed)
}

略式の引数名

クロージャーには自動的に引数に名前がつけられています。第一引数は$0、第二引数は$1、第三引数は$2といった具合です。
これを使うと、引数の定義をしなくてもいいし、 inの文字も必要ありません。

let reversed = names.sort({ $0 > $1})
print(reversed)
//->["Ewa", "Daniella", "Chris", "Barry", "Alex"]

オペレーター関数を使う

最後に、省略形の最終形態。オペレーター関数を使う場合をみてみます。
SwiftのString型は > オペレーター関数が使えます。 > はここでは2つのString型引数をもち、戻り値としてBool型を返す関数です。sort(_:)も2つのString型をもち戻り値としてBool型を返す関数を引数にしています。
これが組み合わせると以下のようになります。

let reversed = names.sort(>)
print(reversed)
//->["Ewa", "Daniella", "Chris", "Barry", "Alex"]

names.sort(>)と書くだけで、引数、戻り値が推論されて、names配列がソートされています。
このような書き方もできることは覚えておくといいかもしれません。

Trailing Closures

SwiftにはTrailing Closuresと呼ばれるクロージャーの書き方があります。  
関数の引数の末尾(最後)がclosureを受け取る場合、処理部分を外に配置できる書き方です。

sort(_:)メソッドででもTrailing Closuresを使ってみましょう。

var reversed = names.sort(backwards)
reversed = names.sort(){
    $0 > $1
}
print(reversed)

先ほどまではnames.sort({$0 > $1})のように()の中にクロージャーを入れていましたがTrailing Closuresではnames.sort(){$0 > $1}と{}が外出しされているのがわかると思います。
コードがちょっと楽になる書き方です。

Trailing Closuresで、引数がクロージャー以外ない場合は()を省略できます。

reversed = names.sort{
    $0 > $1
}
print(reversed)

このように書くこともできます。


配列要素に対する操作

Array型、set型、dictionary型には、クロージャを適応してインスタンスを操作するメソッドが用意されています。

各メソッドの使い方を紹介します。

filter

SequenceTypeプロトコルで定義。クロージャで指定した条件に合った要素で構成されたArray型を返します。

//filter : クロージャを適用して、trueを返す要素だけを取り出して新しい配列を返す
let intArray = [1, 2, 3, 4, 5]
let evenArray = intArray.filter {
    $0 % 2 == 0
}

print(evenArray)
//->[2, 4]

//Dictonary型
let dic = ["a" : 1, "b": 2]
let filterDic = dic.filter() {
    $0.1 % 2 == 0
}
print(filterDic)//タプル型の配列として返ってくる
//->[("b", 2)]

map

CollectionTypeプロトコルで定義。クロージャで指定した要素で構成されたArray型を返します。

//map:クロージャを適用して、その結果を新たな配列として返すメソッド
let intArray = [2, 80, 45, 13]
let stringArray = intArray.map{
    "\($0)"
}
print(stringArray)
//->["2", "80", "45", "13"]

//Dictonary型
let dic = ["a":1, "b": 2]
let dicmap = dic.map() {
    ($0.0, $0.1)
}
print(dicmap)//タプル型のArrayが返ってくる
//->[("b", 2), ("a", 1)]

sort

Array型はMutableCollectionTypeプロトコル、Dictonary型とSet型はSequenceTypeプロトコルで定義。クロージャで指定した条件で要素を入れ替えます。

//sort: 配列前後の要素を比較し、trueなら要素を入れ替え、falseならそのままにすることで新しい配列を返す
let intArray = [1, 2, 3, 4, 5, 10, 20, 30, 111, 112]
let sortEvenArray = intArray.sort() {
    //偶数を順番に並べた後に、奇数を順番に並べる
    $0 % 2 == 0 && $1 % 2 != 0
}
print(sortEvenArray)
//->[2, 4, 10, 20, 30, 112, 1, 3, 5, 111]

//Dictionary型でsort
let dic = [1: "ss", 33: "dd"]
let sortdic = dic.sort() {
    $0.0 < $1.0
}
print(sortdic)
//->[(1, "ss"), (33, "dd")]

//Set型でsort
let set : Set<Int> = [1111, 1, 2, 4, 6]
let setsort = set.sort(){
    $0 < $1
}
print(setsort)
//->[1, 2, 4, 6, 1111]

reduce

SequenceTypeプロトコルで定義。クロージャで指定された処理を各要素に適応し適応結果を返します。

//reduce: 初期値を引数に指定して、要素前後の操作をクロージャで適応するとその結果を返す
let intArray = [Int](1...10)
let sum = intArray.reduce(0){
    $0 + $1
}
print(sum)
//->55

let sampleSet :Set<Int> = [1, 20, 30, 40]
let sumSet = sampleSet.reduce(0) {
    $0 + $1
}
print(sumSet)
//->91

組み合わせる

map,filter,sortのメソッドは戻り値が配列となるので、ドットでつなげて実行することができます。

以下はmap,filter,sort,reduceを組み合わせて実行している例です。



let dat = [2, 80, 45, 13]

//10以上を昇順に並べて文字列に変換する
let strArray = dat.filter{ $0 >= 10}.sort{ $0 < $1}.map{ "\($0)" }
print(strArray)

//10以上の要素を全て足す
let sumThanTenNumber = dat.filter{ $0 >= 10}.reduce(0){ $0 + $1 }
print(sumThanTenNumber)

最後に

Swiftのクロージャーは略式が多くて、最初は戸惑うかもしれません。 
色々な書き方を見比べて、早く慣れましょう!