このところ見ていっている オプショナルバインディング
のお話は、あと何回か続くとは思いますけれどそろそろ大詰め。今回は if var
表記や 強制アンラップ
周りを見ていくことになりそうです。どちらともこれまでに オプショナルバインディング
を見ている中でたびたび触れた話題ですけれど、おさらいとしてそれに主眼を置いて眺めてみましょう。
今回もゆめみ社外の人を招いての開催で、一般の方も若干名参加してくださる見込みです。どうぞよろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #163
00:00 開始 01:18 前回のおさらいと今回の展望 03:15 値型と参照型の代入における違い 05:43 渡された値に変化を与えることを考える 06:29 変数でシャドーイングして書き換えて返す 08:10 Copy-in Copy-out の捉え方 09:15 inout による Copy-in Copy-out の実現 11:07 参照型における参照渡し 13:44 引数と Copy-in Copy-out 14:17 クロージャーとクロージングオーバー 16:03 スコープを超えて変数を扱う 18:50 クロージングオーバーされた値は共有される 20:04 関数型に weak は付けられる? 21:49 クロージャーの延命とキャプチャー ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #163
はい、では始めていきましょう。今日はオプショナルの話をしようと思っています。ここ最近、オプショナルの話を続けているので、その続きをしようと思っていました。しかし、前回のコードをもう少し補足することも有意義かなと思い直したんです。それもあって、オプショナルの話から少し逸れますが、深掘りする機会ってそう多くないので、今回は前回の続きをやってみます。
前回は、構造体のインスタンスをクロージャーでキャプチャーした時に、それがどのように参照されるかという話をしていました。まずこの違いを解説したいと思います。クロージャーで構造体をキャプチャーした場合とクロージングオーバー(capturing the enclosing variables)した場合で、動作が大きく変わりますので、その違いをしっかりと理解してほしいと思います。そのための細かい動きを見ていこうと思います。
まず構造体の例を使って説明しますね。例えば、構造体A
とB
があり、配列を使って説明します。以下のようにします。
struct A { var array: [Int] }
var a = A(array: [1, 2, 3])
var b = a
a
とb
を表示すると、両方共 [1, 2, 3]
と表示されます。ここでb
に対して要素を追加すると、例えば次のようにします。
b.array.append(4)
すると、Swiftでは構造体は値型として扱われるため、b
に新しい要素が追加されてもa
には影響を与えません。a
は元の配列 [1, 2, 3]
を維持し、b
は新しい配列 [1, 2, 3, 4]
となります。
これが構造体の代入時における動きです。Objective-Cの場合は構造体のコピーが押されるような動作になります。Swiftでは、構造体(値型)を代入するときに自動的にコピーしますが、参照型の場合はコピーをしないという違いがあります。
次に、クロージングオーバーの話をします。関数内で変数を渡すときの動きについても説明しますね。例えば、以下のような関数があるとします。
func modifyArray(_ array: inout [Int]) {
array.append(4)
}
そして、この関数を使って配列を渡します。
var a = [1, 2, 3]
modifyArray(&a)
print(a) // [1, 2, 3, 4] と表示されます
この場合、配列はinout
パラメータとして渡されるため、コピーされるのではなく直接参照されます。結果として、元の配列も変更されます。
片や、渡された配列がコピーされる場合です。
func modifyArray(_ array: [Int]) {
var newArray = array
newArray.append(4)
}
この場合、newArray
はコピーされたものであり、元の配列には影響を与えません。このため、元の配列は変更されず、新しい配列のみが変更されます。Swiftでは、このように値型
と参照型
、そしてクロージングオーバー
やキャプチャ
の違いを理解することが重要です。
これらの違いをしっかりと理解し、適切な方法で使い分けることで、より安全で効率的なコードを書くことができると思います。 なので、このような場合には「ロード」のアイディアとして、Int
型の配列を返すようにして、ここで C
をリターンします。そして B
に戻り値として上書きし、これで変更を反映させる方法が一つあります。これにより、[1, 2, 3, 4]
になります。これがコピーイン・コピーアウトの基本の形です。
関数に渡すときに、B
を渡したときに B
の複製が C
として作成されます。これがコピーインです。複製が作成され関数に入ります。そして、関数がリターンすると、リターンされた結果がインスタンスとして B
に上書きされることになります。オブジェクト指向的に言うと、このようにコピーされたものと同じような雰囲気になります。この方法でコピーが作成され出力されます。これがコピーイン・コピーアウトの基本的な概念と思います。
次に、inout
を使うことによって、値の書き換えができるようになります。そうすると、戻り値を返さなくても [1, 2, 3, 4]
となります。
最近、Swiftのプレイグラウンドが途中で動きをやめることがあるのですが、これはバグだと思います。この場合、inout
を使って参照渡しを行い、パラメータを渡しただけで基本的に新しい値が反映されるようになります。inout
は参照渡しではなく、実際にはコピーイン・コピーアウトになっています。そのため、関数に渡された時にはコピーが作成され、終了時には戻り値のコピーが作成されます。これが基本原則です。
最適化されると話は変わりますが、それは置いておいて、同様に、クラスの場合は少し違ってきます。配列ではなく独自の型を使うとして、例えばバリュー型のstruct RowValue
のようにします。
struct RowValue {
var value: Int
}
var a = RowValue(value: 0)
var b = a
ここで、inout
を使ってバリュー型を渡し、その中の値を1
に変更します。このとき、b
を渡すことにより、a
とb
の値はどうなるかを見ていきます。
コードの間違いを修正して再実行すると、RowValue
の値が 0
から 1
に変更されたことがわかります。クラスの場合と違い、inout
を使わずに値を変更できます。
次に、クラスの場合、参照型になるので、値を変更するために inout
は必要ありません。
class RowValueClass {
var value: Int
init(value: Int) {
self.value = value
}
}
var c = RowValueClass(value: 0)
var d = c
d.value = 1
これで、クラスの場合は参照型なので、両方の変数に変更が反映されます。また、クラスの値が見づらい場合は、description
プロパティを追加することで改善できます。
extension RowValueClass: CustomStringConvertible {
var description: String {
return "value: \\(self.value)"
}
}
これで、c
とd
の値が同じであることがわかります。クラスの場合は、参照型なのでコピーイン・コピーアウトを必要とせず、参照渡しがデフォルトです。
最後に、クロージャの話ですが、クロージャを使う場合も示します。例えば、引数を取らずにクロージャ内で変数b
を操作する場合、以下のようになります。
let closure = {
b.value = 2
}
closure()
これで、b
の値が変わります。クロージャのスコープ内の変数を参照できることを、クロージングオーバーと言います。これにより、特定のスコープ内の変数をクロージャ内で使用することができます。これがクロージングオーバーの概念です。一般的にはキャプチャーと呼ばれることもありますが、詳細にいうとクロージングオーバーです。 これによって、クロージャの外にあるものに影響を与えることができます。例えば、次のような関数を考えてみましょう。
func getFunction() -> () -> Int {
var x = 0
return {
x += 1
return x
}
}
この関数では、x
という変数がクロージャ内で閉じ込められています。このgetFunction
を呼ぶと、クロージャを返します。このクロージャを実行すると、x
が1増加して返されます。
たとえば次のように使います。
let g = getFunction()
print(g()) // 1
print(g()) // 2
print(g()) // 3
この例で、g
を実行するたびに、x
が保持されて1ずつ増加することがわかります。通常、関数のローカル変数はその関数のスコープを超えると消えてしまいますが、このクロージャによって変数x
はスコープを超えて生き続けます。
さらに面白いところは、変数x
の値がクロージャで書き換えられて、その状態がちゃんと継続することです。別の関数としてコピーすると、次のようになります。
let h = g
print(h()) // 4
このように、g
とh
が同じクロージャを参照しているので、変数x
の変更が共有されます。構造体にもかかわらず、クロージャのキャプチャによって変数x
が残っているのです。
さて、ここからリストの話に移りたいと思いますが、少しコメントを拾っていきます。「クロージャにウィークをつける理由」についてのコメントがありますね。通常、クラスを渡すときの強い参照で循環参照を避けるためにweak
を使います。しかし、クロージャも参照型なので、このような場合にweak
を使えるかもしれませんが、Swiftではクロージャそのものにweak
を付けることはできません。
その点について深堀りしてみます。「クロージャにweak
をつけると面白かったのですが、エラーが出るため、オブジェクトが解放されることになりますね」といった話題が出ていました。weak
はクラスのプロトコルにしか使えないので、クロージャには使用できません。また、エスケープクロージャを使うと、生存時間が延びるため、その中でキャプチャされたものが気になるでしょう。このような場合でも、クロージャは参照型です。
非常に興味深い話題で、クロージャと変数の寿命、メモリ管理について理解が深まりました。 同じ章でまた変わったことがありますよね。アノテーションが付いていた気がします。アプリレートかアノテーションを見れないですかね?見てみましょうか。いや、アプリケーブルしか付いていないですね。クロージャーにも付いていないです。これは、センダブルなら大丈夫ということかな?でも、センダブルではないですよね、クロージャーは。多分、センダブルを付けないとダメですね。センダブルにしたいなら、引き出せないということですね。引き出せないということがコンパイラには分かっているから、求められてない。でもここで await
していますね。ちょっとスレッドを変えてみましょう。
それか、なんでエスケープがいらないんでしょうかね?そういうものなのかな。調べてないので分かりませんが、invoke async
を読んであげると、await
が出てきて動きますよね。これも動きますね。こうやって print
してタスクを持っていこうかな。これ、あれですね、スレッドスリープとかしてみますか。関係ないですね、パンチが効いているのか。関係ないとは思いますが、一応。
暗黙のセルフキャプチャーですね。セルフとはまた違うのかな。クロージャーなので、何かしらの問題がありますね。しかし、await
で実行されるのが全部タスクなので、タスクユニットが全部オペレーションエスケーピングされているから問題ない。ただ、タスクがすでにオペレーションエスケーピングしているから、大丈夫かもしれません。その可能性もありますね。この実験ではダメだったけれど、ディスパッチとかを使えばいいかもしれません。
話を戻すと、エスケーピングクロージャーの参照型だからクロージャーにエスケーピングを付ける必要があるということかもしれません。確かにコメントでいただいた通り、エスケープしちゃうからね。このまま読んですぐ使い終わる分には、問題ないです。ただ、そうでなければ、クロージャーをクロージングオーバー的に使う場合ですね。
キャプチャーリストについて話します。クロージャーを読むと、1, 2, 3, 4, 5
みたいに、getFunction
で X
をクロージングオーバーして使っていますが、ここでキャプチャーリストとして X
とすると、意味が違ってきます。今使えるかどうかは分かりませんが、結局キャプチャーしてしまうと、完全に独立した X
になってしまいます。コピーなどを取る必要が出てくるので、すべて 1
になってしまいます。こういうふうにね。
このようにキャプチャーリストを使わないと、クロージングオーバーみたいに動きが違うので、注意して使う必要があります。クロージャーを応用的に使う際には気を付けないといけない点ですね。
とりあえず、今日はキャプチャーとクロージングオーバー、そして参照渡しについて話しました。実際に参照渡しやアドレスのチェックなど、どういう感じかまでは話せませんでしたが、それはそれで学術的な話なので、いいでしょう。
今日はこれで終わります。お疲れさまでした。ありがとうございました。