今回も引き続き Swift 5.9 で新たに導入された機能まわりを寄り道して見ていく回にしますね。前回に混乱したまま終わった 所有権
をあれから調べて見ていて、要所が掴めたような予感がするので今日はそれを紹介しつつ、またあらためて 所有権
の様子を Playground で窺ってみようと思ってます。よろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #302
00:00 開始 00:09 今回の展望 00:52 前回の話で肝心なところ 01:21 Any には複製しないと入れられない 05:25 複製不可な値は型パラメーターで扱えない 06:15 存在型には複製が内包される 09:54 所有権の、存在型との相性の悪さ 10:54 通常の引数に渡すときは暗黙的に複製する様子 11:29 複製できない値の扱いを指定する手段と捉えれば良さそう 12:40 inout の所有権的な側面は? 15:13 所有権が指定されない場面で、従来通りの動きになる 18:02 borrowing と consuming の概要整理 20:07 無印の値は暗黙複製、消費用の値は明示複製 21:38 キーボードショートカットによる部分的コメントアウト 23:41 所有権の感覚が幾分掴めてきた印象 24:09 return は consume 扱い 25:37 唯一のものに対して、誰がその主導権を握っていくか 26:07 所有権の明記がなければ従来通り— なのもポイント 28:09 所有権の要所まとめ 33:49 クロージング —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #302
さて、今日は引き続きオーナーシップの話をします。前回は非常に混乱して終わりましたが、自分で勉強する限りはそんなに難しくないはずなのに、触ると混乱するというのを何度か繰り返しています。前回もその一つでしたが、これを機にオーナーシップの基本的なところをまとめてみようと思います。
まず、前回の混沌とした部分を振り返りながら、そこからまとめを見ていこうと思います。確かめた重要なところを思い出してみると、コピーできない値型(non-copyable value type)の話がありました。
所有権はジェネリック対応がまだ非常に貧弱です。そのため、何かおかしいことが起きることがあります。例えば、非コピーな値型(non-copyable type)の場合です。具体的には、Value
という非コピーの型があるとします。このValue
型のインスタンスを作って、何かAny
型の変数にそれを格納しようとすると、入らないのです。要は、非コピーな値型は存在型に入れられないという制約があります。
前回もこれが大きな引っかかりでした。例えば、print
関数の引数がAny
型だからです。print
関数にValue
を渡すと、Any
型にキャストしようとしてエラーになります。そのため、非コピーな存在型は現在のところ制約として作れない状況です。
この問題は、Swiftの根本的な特性である「全てのインスタンスは持ち列可能である」ことに反しています。非コピーな存在型を作ることができないため、これは大きな制約となります。また、String Interpolation
を使っても同じ問題にぶつかります。Any
型を取ろうとしてエラーが発生します。
現在のところ、非コピーな存在型ができないのは現実的に避けられない状況です。ですが、将来的にはこれを解決する動きがあるでしょう。今のところ動く範囲で使うしかありません。
また、CustomStringConvertible
というプロトコルがあります。これは、標準的なString
のコンバーティビリティを提供しますが、独自にString
生成をカスタマイズできるものです。このプロトコルも前回触れましたが、一部のケースではうまく対応できないことがあるようです。
ジェネリックを使った例でも試してみます。例えば、あるAction
というジェネリックな関数がValue
を引数に取ろうとした場合、これもエラーになります。Any
相当の型を取ることができないため、ノンコピアブルな値は渡せません。
前回混乱したのは、このジェネリックと非コピーな値型に関する部分が主な原因でした。また、借用(borrowing)という概念も理解が不十分でした。この辺りが問題だったようです。
例えば、関数でValue
の借用を行おうとした場合、その中でprint
することができません。これが借用の制約です。具体例として、関数があって、その中でValue
を取るだけの場合、消費されてしまうことを避けるために借用を行おうとしますが、これもエラーとなります。
本日はこれらの点を整理し、引き続きオーナーシップに関する基礎を学び進めていきたいと思います。 「ここで適当なことをやると、エラーになりますよね。なぜおかしいのかというと、借用したものを内部で保持しようとすると問題になります。それを何かに保持しようとすると解放してしまうからです。これはダメなんですね。何か使おうとするとき、こういった性質があります。借用を内部で保持しようとする場合はコピーしなければならないという点が重要です。こういったことがあるので、意識して借用を付けることはできないんですね。試してみてもダメです。
要は、コンシューム(消費)しようとするものです。解放されたものを代入しようとすると、コピーしなければならないという特徴を忘れずに。プリント文を実行すると消費されてしまうからエラーになる理由がわかります。プリントはany
を取ろうとするので、この場合は11行目でテンポラリ変数としてany
型にキャストしようとします。その時に消費されるのです。
コピーして保持する方法もあります。今、any
の扱いが非常に悪いです。将来的には改善されるかもしれませんが、現状ではany
で全てを扱うことはできません。こうやって値を読んであげると大丈夫ですが、any
にするとまた問題が発生します。x
をコピー不可な値として保存しようとすると、所有権の指定が必要になります。
所有権についてですが、呼び出し元の所有権を保ったままにしたい場合は借用(ボロウィング)を使い、呼び出し先に所有権を渡したい場合は消費(コンシューミング)を使います。一方、インアウト(inout)を使うと、関数に渡す際に所有権が一時的に関数側に移り、最後に元に戻るという形になります。
例えば、x
を借用で渡してインアウトにしようとしている場合、呼び出し元の所有権のまま渡すことは難しいです。これをコンシューミングにして渡し、関数の中で呼び出すと辻褄が合います。
説明の理解を助けるために、消費して別のインスタンスを入れればバリニシャライゼーション
が必要になります。また、具体的にvar n = 0
と設定し、n
を使って値を操作すると、問題なく動作します。」 所有権の細かい動きについては詳細な部分までは分かりづらいかもしれませんが、基本的には一度消費して所有権が移動し、処理が終わった後に所有権が元に戻る動きになります。所有権がどう動いているかをきれいに理解することが大切です。
何も指定しないときには、今までのSwiftの概念に基づいて動きます。値型は暗黙のコピーが行われ、参照型は参照が行われます。少し低レベルなイメージを持っている人は、ポインターの暗黙コピーと考えてもらえればいいでしょう。つまり、=
は暗黙のコピーを意味します。
ボローイング(借用)は、自分の所有権を持ったまま相手に渡すため、書き換えることはできません。相手がそれを保持したい場合はコピーする必要があります。Objective-CのARC(Automatic Reference Counting)では、リテインなしで渡すことを理解するといいかもしれません。そして、参照渡しのような感じでボローイングを行います。
ボローイングでは相手がリードオンリーの参照を得る形になり、所有権を得るためにはコピーが必要です。この辺りを理解できるようになると、参照渡しとコピーの違いや所有権の扱いが明確になります。
一方で、コンシューミングは参照渡し的ですが、所有権が呼び出し先に完全に渡ります。そのため、呼び出し元では扱えなくなります。これはC++のmove
渡しのようなイメージです。呼び出し先が消費することができ、保持することもできます。
実際にコンシューミングの例を見てみましょう。以下のコードでは、コンシュームの操作で変数の所有権が移り、新しい変数に代入されます。
var value = 10
func consumeValue(_ value: inout Int) {
// 値を消費する
}
consumeValue(&value)
この例では、コンシューミングが起きて所有権が移動し、元の変数は使えなくなります。また、ボローイングの場合はエラーになります。消費可能であるコンシューミングの特性を利用して、所有権の移動をしっかり認識しておくことが大切です。
少し例外もありますが、例えばinout
を通じて関数に値を渡す場合、コピーが発生する場合があります。コンシューミングを意識しながらも、細かな挙動を観察する必要があります。このように、所有権の概念をしっかりと理解しながら、最適なコードを書けるようにしましょう。 コンシューミングのものを代入すると消費するから元のものが使えなくなるのは、渡し方でも同じですね。それはすごいことですね。選択して コマンド + /
でコメントアウトするとこうなるんですね。昔からそうだったのでしょうか?便利ですね。
たまたま見つけたのですが、選択しないでコメントアウトするとどうなるか試してみました。面白いですね。時々、さっきの例などをコピーして消していたので、わざわざコピーする必要があったのでしょうね。これで簡単にコメントアウトできるのは便利です。複数行選択も少し面倒ですが、Xcodeで複数行選択できるようになったという話もありますよね。まだ詳しくわかっていませんが、カーソルを使って選択する方法があったはずです。後でツイッターで調べてみるといいかもしれません。
さて、所有権の話です。消費は当然のこととして、消費したくない場合はコピーを使いますね。所有権がどこにあるかがスコープ内で変わってくるというのは重要なポイントです。リターンするときのインスタンスは所有権を呼び出し元に移すという形で消費されることになります。ボロイングが消費できないというのも重要です。リターンする場合、そのままではできないのでコピーする、というのもポイントです。
このように、所有権の大事な部分は唯一のものに対してどちらがコントロールを持つかということです。それが1つのものに対してどこで使えるかという理解になります。注意すべき点として、コンシューミングの仕組みやリターンの際の動作などの観点は大事です。
通常のパラメーターに渡すときも明示しない限りはコピーされる動きになります。これは今まで通りの動作なので、新しいものに注意する必要はありません。
では、イニシャライザーについて少し話します。イニシャライザーに渡るときにボロイング扱いとなるかどうかを検証してみましょう。例えば、イニシャライザーがあって、バリューが値型で渡る場合、それがコンシューミング扱いになるかどうかです。バリュー型は通常コピー可能ですので、イニシャライザー内でもコピーされます。Boxに入れるなどの操作を考えたとき、消費されないことを確認できます。コピーが行われているので、機能として問題ないですね。
これがどういう動きになるかをきちんと確認することで、所有権についての理解がより深まります。所有権やコピーといった概念は、プログラミングにおいて重要なトピックですので、引き続き実践を重ねながら学んでいきましょう。 今まで話してきた内容で、概ね理解できたと思いますが、オーナーシップについてもう少しまとめてみたいと思います。スライドにも要点を作成しましたので、以下に説明します。
まず、オーナーシップとは所有権をどのように管理するかということです。所有権の指定は主に「ボローイング」と「コンシューミング」に分けられます。ボローイング(借用)は、所有権を自分のままにして相手に一時的に利用させることです。一方、コンシューミング(消費)は所有権を相手に渡して、そのまま消費してもらうものです。
「インアウト」についても先ほど説明しましたが、所有権を渡して最後に書き戻してもらい、再び自分が所有権を持ち直す動きです。コピーイン・コピーアウトのように、相手にコピーを渡し、自由に書き換えさせて、最後に完成品を受け取ることでアトミックな状態――つまり、他の状態に影響を受けない一貫した状態――を作り出します。これにより、レースコンディション(競合状態)を回避する一つの方法となります。
普通の渡し方については、値型は複製されますが、参照型は参照またはポインタとして複製されます。もちろん、クラスでは唯一のインスタンスを用意するため、その管理はクラスの方で行いますが、値の受け渡しには関与しません。
セルフについても同様に、ボローイング(借用)、コンシューミング(消費)、インアウト(イン・アウト)の指定があります。ボローイングファンクションならば、セルフはボローイングされ、コンシューミングファンクションならばセルフはコンシューミングされます。さらに、エミュレーティングファンクションの場合はセルフはインアウトとなります。
例えば、ボローイングファンクションでセルフを他に代入しようとすると消費できないよと言われる場合があります。代入が必要ならばコピーが必要となります。このように、所有権をいろいろな方法で管理し、必要に応じて消費したり複製したりすることで、適切なメモリ管理を行います。
ディスカードセルフを使用すると、自分自身を初期化状態に戻さずにそのまま放棄することもできます。これは今日の話の範囲外ですが、どこかで役立つことがあるかもしれません。
まとめとして、今日話した内容を押さえてもらえれば、主要なポイントは理解できると思います。所有権管理について疑問があれば、次回の勉強会でさらに詳しくお話しする機会を設けたいと思います。
最後に、時間が来ましたので今日はここまでにします。お疲れ様でした。ありがとうございました。