Swiftの後置クロージャ(Trailing Closures)

クロージャ

後置クロージャ(Trailing Closures)というのは、クロージャ本体(波括弧{}で囲まれた部分)を、カッコ()の後ろに置いたものです。後ろに置くので後置(こうち)クロージャと呼びます。最初に紹介したクロージャ表現では、括弧の中に置いていましたが、これはインラインクロージャと呼んでました。ここでは、後置クロージャについて簡単に説明した後、具体例をサンプルコードで紹介したいと思います。

後置クロージャ(Trailing Closures)って何?

インラインクロージャでは、本体を括弧()の中に書きました。これがちょっと嫌だなと思う人は、これから紹介するTrailing Closuresという書き方が使えます。日本語訳を当てると、後置クロージャ、または、クロージャ後置記法、でしょうか?ここでは、英語表記のtrailing closuresをそのまま使います。

先程まで一生懸命簡略化したインラインクロージャを、trailing closureで書き直してみます。

// Trailing closureの例
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

var reversedNames = names.sorted() { $0 > $1 }
// または
reversedNames = names.sorted { $0 > $1 }
print(reversedNames)
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]、と表示

このように、インラインクロージャではカッコ()の中に書いていた部分を、後ろに置いて書き直すことができます。

また、2番目の例のように、クロージャ表現のみが関数やメソッドのパラメータである場合、関数(メソッド)呼び出しのカッコ()を省略することができます。

Trailing Closuresって、どういう時に使うのか?

公式マニュアルにも書いてあるように、trailing closuresが役に立つケースは、クロージャ本体の記述が長くなるような場合です。余り長くなりすぎると、インラインクロージャで書いた場合、可読性が悪くなってしまうからです。そういう意味では、sorted()メソッドの例では、わざわざtrailing closureを使わなくても良いかもしれません。

ここでは、公式マニュアルに載っている、コレクション型のmap(_:)メソッドの例を流用して説明します。先ずは、全体のサンプルコードを示してから、それぞれのパーツ毎に詳しく見ていきます。

// コレクション型のmap(_:)メソッドを使った、trailing closureの使用例

// 数字(整数)と文字列の対応(Dictionary)
let digitJapanese = [
    0:"零", 1:"一", 2:"二", 3:"三", 4:"四",
    5:"五", 6:"六", 7:"七", 8:"八", 9:"九"
]

// 文字列の配列を生成するためのインプット
let numbers = [2015, 76, 123]

// map()メソッド+trailing closureで、文字列型の配列を生成
let strings = numbers.map {
    (number) -> String in
    var number = number
    var output = ""
    while number > 0 {
        output = digitJapanese[number%10]! + output
        number /= 10
    }
    return output
}
print(strings)
//["二零一五", "七六", "一二三"]、と表示

この例は、整数から文字列への変換を提供するmap(_:)メソッドの実装になります。順番に説明します。

整数から文字列に変換する対応表(辞書)を作成

ここで実装したいのは、各桁毎の数字(例えば2とか)を、それに対応する文字列に変換する機能です。したがって、0から9までの整数に対応した文字列を、Dictionaryコレクション型で作っておきます。

// 数字(整数)と文字列の対応(Dictionary)
let digitJapanese = [
    0:"零", 1:"一", 2:"二", 3:"三", 4:"四",
    5:"五", 6:"六", 7:"七", 8:"八", 9:"九"
]

これは簡単です。公式マニュアルそのままでは、ちょっとおもしろくないので、出力側の文字列を日本語にしました。

mapメソッドに実装するtrailing closureのパラメータと戻り値

map(_:)メソッドが取るfunction typeは、コレクション型が保有している型(ここではInt)を任意の型(ここではString)に変換する型、つまり

(someType in collection) -> anotherType

のようなfunction typeになります。ここで、map(_:)の戻り値はanotherTypeになります(詳しく知りたい方は、先程紹介したiOS Developer Library: SequenceType Protocol Referenceのページで、mapを検索すると良いかもしれません)。

今回の例ですと、function typeは(Int) -> Stringで、戻り値はStringです。そういう視点で、先ほどの実装部分を見てみると、

 ....
let strings = numbers.map {
    (number) -> String in // function type: (Int) -> String
    var number = number // numberは整数
    var output = "" // String
    ....
    return output // 戻り値: String
}

確かに、function typeとしては(Int) -> Stringをパラメータとして、戻り値はStringです。ここで、パラメータのnumberには、型に関する指定が何もありませんが、インプットである整数型Array(numbers)からの型推論によって、Intと決定されています。

また、numberはクロージャ内部で変更したいので、定数から変数へ変換しています。

// 定数から変数へ変換(クロージャのパラメータは定数のみ)
....
var number = number
....

ちょっと特殊な処理のようですが、Swift 3へのアップデートで、クロージャが変数パラメータを取れなくなることを考慮した書き方です。

Swift 2.2では変数パラメータを取れますが、warningが出ます。上記のような書き方はややこしいですが、構造体やクラスなどがパラメータからプロパティへ値を渡すのと似ています。ここではパラメータからローカル変数への値渡しになります。

さらに、map(_:)は、パラメータとしてクロージャしか取りませんので、最初に説明したように、map(_:)のカッコ()を省略することが可能です(もちろん、明示的にカッコをつけても問題ありません)。

整数を文字列に変換する機能の実装

後は、整数を文字列に変換する部分です。

....
var output = ""
while number > 0 {
    output = digitJapanese[number%10]! + output
    number /= 10
}
....

この処理では、一の桁から順番に、一文字ずつ整数を文字列に変換しています。少し複雑ですが、下の図と合わせて説明します。

[図解]Trailing Closure:整数・文字列変換

先ず、最初の剰余演算number%10で、一桁目の整数3を取り出します。これを、dictionaryの添字に入れて、対応する文字列である"三"を取り出します。ここで、dictionaryはオプショナル(optionals)で包まれていますので、forced unwrapping!を付けて取り出しています。

今、入力した整数の一桁目から取り出していますが、出力の文字列の並び順は入力と同じにしないと困ります。したがって、output = "三" + outputという風に、出力文字列の先頭に文字列を付け足していく演算になっています。output = output + "三"という順序ですと、最終結果は"三二一"になります。

最後に、number/10で、整数の桁を一つ落とします。例えば、123/1012を返します。浮動小数点型ではないので、12.3という数字は、小数点以下が切り捨てられて12になります。桁を落とした後は、ループの先頭に戻ります。このwhileループは、number > 0が真の限り続きます。最終的には、入力が一桁になった後、number/10が実行された時点でfalseになります。

まとめ

  • 後置クロージャ(Trailing closures)は、クロージャ本体を括弧()の後に置いた表記記法
  • Trailing closuresは、クロージャ本体が長い場合に有効