本記事は、【Swift】クロージャの使い方〜基礎〜の続きになります。
こちらの内容が理解できない方はぜひ基礎の記事もご覧ください!
変数と定数のキャプチャ
突然ですが「スコープ」をご存知でしょうか?
スコープとは、変数や定数の使用できる範囲のことを指します。
ローカルスコープで定義された変数や定数は、
ローカルスコープ内でしか使用することができません。
(スコープについて理解できていない方は過去の記事をご覧ください。)
一方で、クロージャが参照している変数や定数は、
クロージャが実行されるスコープが変数や定数が定義されたローカルスコープ以外であっても、
クロージャの実行時に利用することができます。
理由としては、クロージャ自身が
定義されたスコープの変数や定数への参照を保持しているためとなります。
この保持する機能をキャプチャと言います。
文だとわかりにくいので例をあげます。
let hello: (String) -> String
do {
let helloWord = "こんにちは"
hello = {(name: String) -> String in
return "\(name)さん、\(helloWord)"
}
}
hello("栗原") // 栗原さん、こんにちは
helloWord // コンパイルエラー
hello("栗原")`の部分で、クロージャの定義されている定数helloを読んでいます。
定数helloは外のスコープで定義しているので参照することが可能です。
一つ気になるのが、定数helloのクロージャの中で、
return "\(name)さん、\(helloWord)"
が実行されています。
この中のhelloWordに関しては、
do{ }の中で宣言されたローカルスコープなので、
hello("栗原")
のスコープからは参照できないはずです。
ですが、クロージャはキャプチャ機能があるので、
クロージャが定義されたスコープの変数や定数への参照を保持しています。
今回クロージャが定義された箇所は、定数helloWordが定義された箇所と同じです。
そのためコンパイルエラーにならずに実行することができるということになります。
しかし、別のスコープからhelloWord
単体にアクセスするといった、
クロージャとは関係のない処理の場合だとコンパイルエラーになります。
引数としてのクロージャ
クロージャを関数や別のクロージャの引数として利用する場合にのみ有効な仕様として、
属性とトレイリングクロージャという仕様が存在します。
属性は、クロージャに対すて指定する追加の情報で、
トレイリングクロージャはクロージャを引数にとる関数の可読性を高めるための仕様です。
属性もトレイリングクロージャも結構ややこしいです・・・!
自分も完璧に理解したわけではないので、
もし間違っていたら申し訳ございません。
属性の種類と指定方法
属性には、@escaping属性
と@autoclosure属性
があり、
クロージャの型の前に@属性名を追加して指定します。
func 関数名(引数名: @属性名 クロージャの型名) {
関数呼び出し時に実行される文
}
escaping属性
escaping属性は、関数に引数として渡されたクロージャが、
関数のスコープ外で保持される可能性があることを示す属性になります。
コンパイラはescaping属性の有無によって、
クロージャがキャプチャを行う必要があるかないかを判断しています。
つまり、引数のクロージャを関数のスコープの外側で保持する場合は、
escapingが必要になるということになります。
例としては下記のようになります。
var user: [String] = []
var sample: [(String) -> Void] = []
var count = 0
func add(name: String, function: @escaping (String) -> Void) {
user.append(name)
sample.append(function)
}
add(name: "Kozima") { (string) -> Void in
print("Hello \(string) !")
}
add(name: "Satou") { (string) -> Void in
print("Hello \(string) !")
}
add(name: "Aoki") { (string) -> Void in
print("Hello \(string) !")
}
sample.forEach { (function) in
let name = user[count]
function(name)
count += 1
}
実行結果
Hello Kozima !
Hello Satou !
Hello Aoki !
〜コードの説明〜
・関数内に関する処理
func add(name: String, function: @escaping (String) -> Void) { ・・・ }
String型の引数と、(String) -> Void型の引数の二つを受け取ります。
user.append(name)
配列userの中にString型の第一引数の値を格納しています。
sample.append(function)
配列sampleの中にクロージャである(String) -> Void型の第二引数の値を格納しています。
ここで、関数のスコープ外に引数のクロージャを保持していますので
@escapingをつける必要が出てきます。
・関数の呼び出しに関する処理
add(name: "Kozima") { (string) -> Void in ・・・ }
add( )から始まる箇所は関数を呼び出しています。
add(name: "Kozima") { ・・・
add( )関数の第一引数に文字列Kozimaを渡しています。
{ (string) -> Void in ・・・ }
add( )関数の第二引数は(String) -> Void型なので、
(String) -> Void型のクロージャを渡しています。
print("Hello \(string) !")
渡しているクロージャの処理の内容になります。
クロージャの引数(String型)を使って"Hello (引数でもらった名前) !"と
表示する処理を行っています。
・forEachに関する処理
sample.forEach { (function) in ・・・ }
配列sampleに対してforEach( )関数を使っています。
forEach( )は、配列の先頭から順次アクセスする関数です。
参照した配列の値をfunctionの中に格納します。
配列sampleの中身ですが、
(String) -> Void型のクロージャが入っている状態になりますので、
functionの中にはクロージャが入っている状態になります。
let name = user[count]
配列userの中には、add( )関数を呼び出した際の第一引数で指定した文字列が順に入っています。
定数nameを宣言し、その中に配列userの文字列を格納しています。
なお、countの値はforEachの最後でインクリメントし、
先頭から順に参照できるようにしております。
function(name)
sample.forEach { (function) in ・・・ } の箇所で、
functionには配列sample内に格納してあったクロージャが入っています。
こちらは、そのクロージャを実行するコードになります。
しつこいですが、クロージャの型は(String) -> Void型なので
引数に先ほど宣言した定数nameを渡しています。
そうすることで、print("Hello \(string) !")
が実行され、
stringには定数nameの値が入るわけです。
escapingの使い方はこのような使い方になります。
正直難しいですよね・・・(笑)
escapingの宣言が必要な時に宣言されていないとコンパイルエラーが発生します。
なので、コンパイルエラーが出た時に@escapingをつけるというくらいの認識でいいと思います!
autoclosure属性
autoclosure属性は、引数をクロージャで包むことで遅延評価を実現するための属性です。
autoclosure属性の素晴らしい機能を説明するために少し細かく説明していきます。
Bool型の引数を2つ受け取り、その論理和を返すor( )関数を実装します。
つまり、||演算子
と同じ処理を行う関数を実装します。
第一引数のlbがtrueの時点で論理和の結果はtrueなので、
lbがtrueだった時点でreturn true
を行っています。
lbがfalseだった場合はrbの結果でtrueかfalseが変わります。
なので、print("\(rb)です。")
とreturn rb
とすることで、
rbの結果を表示しrbの結果をreturnするようにしています。
func or(_ lb: Bool, _ rb: Bool) -> Bool {
if lb {
print("trueです。")
return true
} else {
print("\(rb)です。")
return rb
}
}
or(false, true)
実行結果
trueです。
単純に実装するとこのようなコードになると思います。
次に引数に渡すBool型の値を、関数の戻り値で指定するケースを実装してみましょう。
引数に使う関数は、実行されたことが分かるようにprint( )で表示しておきます。
func or(_ lb: Bool, _ rb: Bool) -> Bool {
if lb {
print("trueです。")
return true
} else {
print("\(rb)です。")
return rb
}
}
func flb() -> Bool {
print("flb()関数が実行されました。")
return true
}
func frb() -> Bool {
print("frb()関数が実行されました。")
return false
}
or(flb(), frb())
実行結果
flb()関数が実行されました。
frb()関数が実行されました。
trueです。
実行結果から、flb( )とfrb( )の両方が実行されていることがわかります。
Swiftは、多くのプログラミング言語と同じで、
関数の引数がその関数に引き渡されるより前に実行されるので、
各関数が実行された後にその結果を引数としてor( )関数が実行されます。
この仕組みを正格評価と言います。
しかし、今回のような処理の場合は、
第一引数がtrueの時点で結果はtrueになるので第二引数の関数は実行される必要がありません。
理想としては、第一引数がfalseと分かった後に実行して欲しいです。
そこでクロージャを使い、必要になるまで第二引数を実行しないような処理にしてみます。
第二引数を() -> Bool型
に変更し第二引数が必要になった時点でクロージャを実行します。
func or(_ lb: Bool, _ rb: () -> Bool) -> Bool {
if lb {
print("trueです。")
return true
} else {
let rb = rb()
print("\(rb)です。")
return rb
}
}
func flb() -> Bool {
print("flb()関数が実行されました。")
return true
}
func frb() -> Bool {
print("frb()関数が実行されました。")
return false
}
or(flb(), { frb() })
実行結果
flb()関数が実行されました。
trueです。
実行結果からわかるように、frb( )関数は実行されておりません。
let rb = frb()
が行われるまで、第二引数の関数は実行されなくなります。
つまり、flb( )の結果がtrueの間は実行されなくなります。
or( )関数の第二引数をクロージャにすることで無駄な処理を省くことができました。
しかし、引数をクロージャにしたため関数を呼び出す時の記述が少し煩雑になりました。
今回のようなケースの、
メリットを残しデメリットを回避するための属性がautoclosure属性です。
先ほどのコードにautoclosure属性をつけると次のようになります。
func or(_ lb: Bool, _ rb: @autoclosure () -> Bool) -> Bool {
if lb {
print("trueです。")
return true
} else {
let rb = rb()
print("\(rb)です。")
return rb
}
}
func flb() -> Bool {
print("flb()関数が実行されました。")
return true
}
func frb() -> Bool {
print("frb()関数が実行されました。")
return false
}
or(flb(), frb())
実行結果
flb()関数が実行されました。
trueです。
関数を呼び出す際のor(flb(), frb())
がかなりスッキリし、
最初の状態と同じになったことがわかります。
autoclosure属性を用いれば、
簡単に遅延評価の実現が可能になることがわかりました。
トレイリングクロージャ
トレイリングクロージャとは、関数の最後の引数がクロージャの場合に、
クロージャを( )の外に書くことができる記法です。
こう聞くと引数なのに( )の中に書かないなんて紛らわしい!
と思ってしまうかもしれませんが、正直かなり見やすくなります。
トレイリングクロージャを使用してない場合と使用した場合を記載します。
func sample(number: Int, message: (String) -> Void) {
message("あなたの番号は\(number)番です。")
}
// トレイリングクロージャを使用しない場合
sample(number: 10, message: { string in
print(string)
}) // 関数の()の終わりがここまで来ている。
// トレイリングクロージャを使用した場合
sample(number: 5) { string in
print(string)
}
実行結果
あなたの番号は10番です。
あなたの番号は5番です。
通常の記法の場合は、関数呼び出しの( )がクロージャの後まで広がってしまいます。
今回のようにクロージャが短ければいいですが、
複数行にまたがる場合の可読性の低さは顕著となります。
一方で、トレイリングクロージャを使用した場合には、
関数の( )はクロージャの定義の前で閉じるため、少しだけコードが読みやすくなります。
また、引数が一つのクロージャのみの関数に対しても同様に、
トレイリングクロージャを使用する場合は関数の( )を省略できます。
func sample(message: (String) -> Void) {
message("こんにちは")
}
sample { string in
print(string)
}
実行結果
こんにちは
クロージャの使い方〜応用〜の説明は以上になります。
escaping属性やautoclosure属性は、
自分もしっかりと理解できていないのでどんどんアウトプットして覚えていきます。
以上、長くなってしまいましたが最後までご覧いただきありがとうございました!