Page icon

第163回

2022/09/28
Empty
番外編
Empty
Empty
Empty
Empty
Empty
13 more properties
このところ見ていっている 
オプショナルバインディング
のお話は、あと何回か続くとは思いますけれどそろそろ大詰め。今回は 
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
になってしまいます。こういうふうにね。
このようにキャプチャーリストを使わないと、クロージングオーバーみたいに動きが違うので、注意して使う必要があります。クロージャーを応用的に使う際には気を付けないといけない点ですね。
とりあえず、今日はキャプチャーとクロージングオーバー、そして参照渡しについて話しました。実際に参照渡しやアドレスのチェックなど、どういう感じかまでは話せませんでしたが、それはそれで学術的な話なので、いいでしょう。
今日はこれで終わります。お疲れさまでした。ありがとうございました。