属性とトレイリングクロージャ
クロージャを関数や別のクロージャの引数として利用する時のみ有効な仕様として、属性とトレイリングクロージャがあります。属性はクロージャに対して指定する追加情報で、トレイリングクロージャはクロージャを引数に取る関数の可読性を高めるための機能です。
属性
属性の指定方法
func 関数名(引数名: @属性名 クロージャの型名) {
関数呼び出した時に実行される文
}
属性の指定方法は、クロージャの型の前に@属性名を追加して指定します。
サンプルコード
下記のでは関数の第2引数の型は、autoclosure属性が指定されたクロージャとなっています。
//引数rhsに属性を指定
func or(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
if lhs {
return true
} else {
return rhs()
}
}
or(true, false) //true
escaping属性
escaping属性は、関数に引数として渡されたクロージャが関数のスコープ外で保持される可能性があることを示す属性です。escaping属性の有無によって、クロージャがキャプチャを行う必要があるかを判断します。そのため、クロージャが関数のスコープ外で保持されなければ、クロージャの実行は関数の実行中に限られるため、キャプチャは必要ありません。一方、クロージャが関数のスコープ外で保持される可能性がある場合(escaping属性が必要な場合)、クロージャの実行時まで関数のスコープの変数を保持する必要があるため、キャプチャが必要になります。
//クロージャを定義
var queue = [() -> Void]()
func enqueue(operation: @escaping () -> Void) {
queue.append(operation)
}
enqueue { print("executed") }
enqueue { print("executed") }
queue.forEach { $0() }
///実行結果
executed
executed
上記のコードでは、直接operationが実行されているわけではなく、配列queueにappendで追加しています。つまり、この引数のクロージャは関数の引数にはescaping属性を指定する必要があり、指定しない場合は、コンパイルエラーになります。
fund executeTwice(operation: () -> Void) {
operation()
operation()
}
executeTwice { print("executed") }
///実行結果
executed
executed
しかし上記のようにexecuteTwice関数内でクロージャを直接実行した場合は、属性を指定してキャプチャしなくてもコンパイルエラーになりません。これはクロージャの実行が関数のスコープ内で行われているため、スコープ外で保持するためのキャプチャは必要なくなります。
autoclosure属性
autoclosure属性は、引数をクロージャで包むことで遅延評価を実現するための属性です。これだけ聞いても分からないと思うので、コードを見ながら考えていきましょう。
func or(_ lhs: Bool, _ rhs: Bool) -> Bool {
if lhs {
print("true")
return ture
} else {
print(rhs)
return rhs
}
}
func lhs() -> Bool {
print("lhs()関数が実行されました")
return true
}
func rhs() -> Bool {
print("rhs()関数が実行されました")
return false
}
//or関数の引数に関数を指定
or(lhs(), rhs())
//実行結果
lhs()関数が実行されました
rhs()関数が実行されました
true
まだ属性は出てきませんが、autoclosureの必要性を確かめるため属性なしのコードから読み解いていきます。
上記のコードではor関数の引数にさらに関数を指定してプログラムを実行しています。その結果上記のような結果が出力されている訳ですが、この結果は本来望むべきものではありません。なぜなら、返り値がfalseであるrhs()の処理も実行されているからです。なぜこのような事が起こるかというと、Swiftでは多くのプログラミング言語と同じく、関数の引数がその関数に引き渡される前に実行されるためです。これを正格評価と言います。では、どうすれば本来実行する必要がないrhs関数を実行せずにすむのでしょうか?答えとしては簡単でrhsの実行を遅らせれれば良いのです。
それでは、必要になるまでは第二引数を実行しないように書き換えてみましょう。
func or(_ lhs: Bool, _ rhs: () -> Bool) -> Bool {
if lhs {
print("true")
return true
} else {
let rhs = rhs()
print(rhs)
return rhs
}
}
func lhs() -> Bool {
print("lhs()関数が実行されました")
return true
}
func rhs() -> Bool {
print("rhs()関数が実行されました")
return false
}
or(lhs(), {return rhs()})
//実行結果
lhs()関数が実行されました
true
実行結果を見ると、不要な関数の呼び出しが行われていないことがわかります。このように、第2引数をクロージャにすることで、必要になるまで評価を遅らせることができるようになりました。これを遅延評価と言います。上記のコードでは、無駄な関数の実行回避ができるというメリットがある一方で、呼び出し側が複雑になってしまうデメリットがあります。そこでここまでのメリットを享受しつつ、デメリットをなくすのがautoclosure属性です。autoclosure属性は、引数をクロージャで包むという処理を暗黙的に行います。結果として、関数外からは最初の列と同様に簡単に利用でき、関数内では2つ目の例のように遅延評価を行えます。それでは属性を使い修正してみましょう。
func or(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
if lhs {
print("true")
return true
} else {
let rhs = rhs()
print(rhs)
return rhs
}
}
func lhs() -> Bool {
print("lhs()関数が実行されました")
return true
}
func rhs() -> Bool {
print("rhs()関数が実行されました")
return false
}
or(lhs(), rhs())
//実行結果
lhs()関数が実行されました
true
or関数の引数がor(lhs(), rhs())となっていますが、実行結果は2つ目の例のようにlhs()関数だけが実行されています。
上記のように、autoclosure属性を利用すれば遅延評価を簡単に実現できます。
トレイリングクロージャ
トレイリングクロージャとは、関数の最後の引数がクロージャの場合は、クロージャを()の外に書くことができる記法です。
func execute(parameter: Int, handler: (String) -> Void) {
handler("parameter is \(parameter)")
}
//トレイリングクロージャを使用しない場合
execute(parameter: 1, handler: { string in
print(string)
})
//トレイリングクロージャを使用する場合
execute(parameter: 2) { string in
print(string)
}
//実行結果
parameter is 1
parameter is 2
通常の記法では関数呼び出しの()がクロージャのあとまで広がり、特に複数行クロージャになると可読性がとても下がります。
一方、トレイリングクロージャを使用した場合には()はクロージャの定義の前で閉じるため、少しだけコードが読みやすくなります。
また、下記のように1つのクロージャのみの関数に対してトレイリングクロージャを使用する場合、関数呼び出しの()も省略できます。
func execute(handler: (String) -> Void) {
handler("executed.")
}
execute { string in
print(string)
}
//実行結果
execute
最後に
解説がとてもわかりにくくなっていると思うので、また機会があらばもっと噛み砕いて説明できればと思います。
今日は疲れたのでここまで、でわでわ!