はじめに
今回はクロージャの中でも特に厄介なキャプチャについてまとめてみます。パート1です。
クロージャのメリット
クロージャのメリットはプログラムの実行中にクロージャのインスタンスを作り出し、定数や変数に代入したり、メソッドの引数として渡したりすることでプログラムの振る舞いを動的に変更することができる点です。
クロージャのキャプチャとは
クロージャはよく無名関数と言われますが、常に同じ機能のインスタンスが作られるわけではありません。インスタンスが生成される際、クロージャの外側にある変数の値を取り込んでインスタンスの一部とし、インスタンスが呼び出される時にはいつでも値を取り出すことができます。これをクロージャのキャプチャといいます。
キャプチャを理解する上で重要なこと
1.
グローバル変数はクロージャの内部からも変数自体に直接アクセス可能。クロージャの内部から変数の値を変更することもできます。
2.
クロージャ式を含むコードブロック内で参照可能なローカル変数は普通にローカル変数として存在している間はクロージャ内からも参照可能で、共通の値が参照、更新できます。コードブロックの実行が終わり、ローカル変数が消滅した後でもクロージャが使われている場合、クロージャの中からは、元のローカル変数が存在し続けているかのように参照、更新が可能です。
2-1.
同じコードブロック内でも一旦実行が終わり、再び実行した時のローカル変数は以前のローカル変数とは別の変数になるため、それらの値が共有されることはありません。
2-2.
あるコードブロック内で複数個のクロージャが同時に生成され、それらが同じローカル変数を参照すると、変数の値は共有されます。コードブロックの実行が終わり、元のローカル変数が消滅した後でもクロージャが使われる場合、それらのクロージャの中からは元のローカル変数が存在し続けているように見え、参照、更新はもちろん、クロージャ間での値の共有ができます。
2-3.
クロージャがクラスのインスタンスなどの参照型の値を持つローカル変数をキャプチャした場合、その値は強い参照で保持されます。クロージャが解放されると同時に、その値への強い参照もなくなります。
クロージャが個別にローカル変数をキャプチャする場合
2-1, 2-2, 2-3の振る舞いを簡単な例を通してみてみましょう。関数makerは()->Intという型のクロージャのインスタンスを一つ作って返す関数です。
var globalCount = 0
func maker(_ a: Int, _ b: Int) -> (() -> Int) {
var localvar = a
return { () -> Int in
globalCount += 1
localvar += b
return localvar
}
}
大切なことは、globalCountは参照されるだけで、localvar, bがキャプチャされるということです。
以下のように二通りの方法で呼び出ししてみましょう。
var m1 = maker(10, 1)
print("m1() = \(m1()), globalCount = \(globalCount)")
print("m1() = \(m1()), globalCount = \(globalCount)")
globalCount = 1000
print("m1() = \(m1()), globalCount = \(globalCount)")
var m2 = maker(50, 2)
print("m2() = \(m2()), globalCount = \(globalCount)")
print("m1() = \(m1()), globalCount = \(globalCount)")
print("m2() = \(m2()), globalCount = \(globalCount)")
出力結果は以下のようになります。
m1() = 11, globalCount = 1
m1() = 12, globalCount = 2
m1() = 13, globalCount = 1001
m2() = 52, globalCount = 1002
m1() = 14, globalCount = 1003
m2() = 54, globalCount = 1004
まず、グローバル変数(globalCount)はm1とm2で呼び出すたびに増加され、共有されていることがわかります。m1を評価した結果を追うと、11, 12, 13, 14と1つずつ増加しています。一方、m2は52, 54となっており、2つ増加しています。異なる値を保持していることから、クロージャのインスタンスのそれぞれに変数localvarとbのコピーが作成されていることが推測されます。(2-1)
複数のクロージャが同じローカル変数をキャプチャする場合
次に、複数のクロージャが同じローカル変数をキャプチャする場合をみてみましょう。関数makerWは二つのクロージャのインスタンスを生成してそれぞれ変数m1とm2に代入したます。二つのクロージャは似た形をしていますが、ローカル変数のlocalvarをそれぞれ1つ増加、5つ増加させて変更後の値を表示します。
var m1: (() -> ())! = nil
var m2: (() -> ())! = nil
func makerW(_ a: Int) {
var localvar = a
m1 = {
localvar += 1
print("m1: \(localvar)")
}
m2 = {
localvar += 5
print("m2: \(localvar)")
}
m1()
print("--: \(localvar)")
m2()
print("--: \(localvar)")
}
次のように呼び出してみましょう。
makerW(10)
m1()
m2()
m1()
出力結果は以下のようになります。
m1: 11
--: 11
m2: 16
--: 16
m1: 17
m2: 22
m1: 23
関数makerWの中で2つのクロージャを呼び出すと、ローカル変数(localvar)の値が共有されていることが確認できます。関数makerWの実行が終わり、ローカル変数(localvar)がなくなっている時点で再び二つのクロージャを呼び出すと、依然として値が保持され、しかも2つのクロージャの間で共有関係が保たれているということです。(2-2)
クロージャが参照型の変数をキャプチャする場合
クロージャが参照型の値を強い参照で保持することの確認をしてみましょう。
整数の値を1つだけ持つクラス(MyInt)を作成し、解放されるタイミングで調べられるようにでイニシャライザを定義しておきます。関数makerZのなかで生成されるクロージャはMyIntのインスタンスを値とするローカル変数(localvar)をキャプチャします。
class MyInt {
var value = 0
init(_ v: Int) {
value = v
}
deinit {
print("deinit: \(value)")
}
}
func makerZ(_ a: MyInt, _ s: String) -> (() -> ()) {
let localvar = a
return {
localvar.value += 1
print("\(s): \(localvar.value)")
}
}
次のように実行してみましょう。
var obj: MyInt! = MyInt(10)
var m1: (() -> ())! = makerZ(obj, "m1")
m1()
var m2: (() -> ())! = makerZ(obj, "m2")
obj = nil
m2()
m1()
m1 = nil
m2()
m2 = nil
出力結果は次のようになります。
m1: 11
m2: 12
m1: 13
m2: 14
deinit: 14
トップレベルで変数obj, m1, m2をオプショナル型として宣言しているのは、nilを代入できるようにするためです。クロージャのインスタンスm1, m2, 変数objが持つMyIntの同一のインスタンスをキャプチャすることになります。このMyIntのインスタンスはobj, m1, m2の全てにnilが代入された時に解放されているのが確認できます。つまり、クロージャはクラスのインスタンスをキャプチャすると自らが存在してる間は強い参照でそのインスタンスを保持しているのです。(2-3)
おわりに
クロージャのキャプチャまとめパート1が終わりました。質問受け付けています!