前回で Swift 5.9 で新たに導入された機能をざっと眺めていきましたけれど、その中でも大々的に打ち出されていてもう少しじっくり見ておいた方が良さそうなものがあるので、引き続きそんな辺りを眺めていきます。理解しきれていないところも多そうなので Playground を使って模索しながら、今回は Swift 5.9 の注目機能とも言えそうな 所有権
あたりをゆっくり見ていこうと思います。よろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #301
00:00 開始 01:42 今回は所有権の話 02:36 所有権の概要 04:56 値型の基本と、所有権でできること 09:18 Copy-in Copy-out と最適化 11:24 所有権に関係する機能 12:31 複製できない値型がまだ貧弱気味 14:36 複製できない値型はジェネリクスでほぼ使えない状況 18:03 複製できない値型の受け渡しは所有権が重要 19:04 複製できない値型は将来拡張予定 19:46 所有権を試してみる 21:05 所有権の意義 23:49 なぜか dropFirst で消費する? 26:58 copy 演算子による明示的な複製 27:21 裏で借用したものを複製している — のを検出している? 30:38 問題の根源を突き詰めていく⋯ 31:32 値は複製されてから消費する 33:36 存在型へのボクシングは消費扱い 34:20 借用した値の扱いがまだつかみきれない 35:00 複製できない値型なら、所有権指定も直感的に働く印象 37:39 今回の所感と次回の展望 —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #301
はい、では始めていきますね。前回からSwift 5.9の変更点についてざっくりと見てきましたが、今回はSwift 5.9のリリースの詳細を見ていきたいと思います。前回は、全体をざっくりと見て、主要な変更点についてお話ししました。この変更点の中でも、主要なものとして「マクロ」と「パラメータパック」がありました。また、その他に多くの細かな更新もありましたね。
本日は、その中でも「オーナーシップ」について詳しく見ていきたいと思います。このオーナーシップについては、この勉強会でも何度か話題に上りましたが、その時はまだ開発途中のSwiftだったため、試せない部分が多かったですね。今回はSwift 5.9でいくつか試すことができるようになったので、このオーナーシップの理解を深めていきたいと思います。
まず、オーナーシップとは何かについて簡単に整理していきます。Swiftでは、基本的に参照型は一つのインスタンスを共有し、値型は必ずコピーして使うというアプローチを取っています。言語が自主的に最適化を図ってくれる場合もありますが、コンパイラが最適化を判断できない場合もあります。そういった場合には、プログラマーが明示的に所有権を指定できるようになったのが、Swift 5.9の新しい機能です。
具体的にどのようなことができるかというと、例えば関数の引数に対して所有権を指定することができるようになりました。以下に例を挙げてみます。
func someFunction(value: Int) {
// 関数内で処理
}
基本的には、このように関数に値を渡す際には、値型(ここではInt
)がコピーされます。しかし、これを制御するために新しいキーワードが導入されました。それがborrowing
とconsuming
です。
func someFunction(borrowing value: Int) {
// `value`は借用のみで変更不可
}
このborrowing
キーワードを使うと、値をコピーせずに関数に渡すことができます。メモリ効率を向上させつつ、安全性も確保できる使用方法です。また、consuming
キーワードも新たに追加されました。
func someFunction(consuming value: Int) {
// `value`の所有権はここで破棄される
}
consuming
を使うと、値の所有権が呼び出し元から呼び出し先に移動します。これにより、再利用されないリソースの開放をコンパイラに伝えます。
さらに、所有権関連のキーワードとしてinout
もあります。これは既存の機能で、関数に値を渡し、その値を関数内部で変更して呼び出し元に反映させるものです。
func someFunction(inout value: Int) {
value += 1
}
このように、所有権に関連するキーワードが増えたことで、プログラマーはより詳細にメモリ管理を制御できるようになりました。これにより、Swiftは更なるパフォーマンスの最適化とメモリ効率の向上が期待できます。
今日は、これらの新しいキーワードとオーナーシップの概念を使って実際にコードを書きながら理解を深めていきましょう。 「インアウトの原則の動きはコピーインとコピーアウトという動きです。これがよく最適化されると、参照が渡って直接書き換えられるような最適化が行われます。Swift 5.8まではそういった最適化が図られていました。コピーにも最適化が行われることが多いですが、これに加えてコンシューミングとフォローイングが新たにできるようになり、さらに最適化が図られるようになるはずです。
しかし、Swiftの所有権については複雑な面もあり、シンプルな面もありますので、その捉え方によって様々な雰囲気が感じられるかと思います。まずは、懸念点についてお話ししておきます。
コンシューミングについては後で詳しく説明しますが、次にフォローイングについても触れます。この2つに関係しそうなものとして、ノンコピーアプリというものもあります。この3つの特徴を使って初めて所有権が生かされるようになると感じています。ただ、現状ではノンコピーアプリがまだ貧弱です。これからもっとパワーアップしてくるのでしょうが、ノンコピーアプリとは何かというと、構造体を原則としてコピーする代わりに、コピーしない構造体を作れるようにするという考え方です。
ノンコピーアプリを活用すると、構造体でありながら非常に最適化された処理のパフォーマンスの良いコードが書けるようになります。通常の構造体やクラス型とは異なり、ノンコピーアプリは所有権を使ってパフォーマンス向上を実現することが目的です。しかし、その威力が本当の意味で発揮されるのは、ノンコピーアプリがしっかりと活かされる場面からでしょう。
現状のノンコピーアプリの貧弱さですが、多くの制約があります。例えば、ジェネリックスでは使えず、総称関数ジェネリックプログラミングでコピーできない値型が使用できません。また、プロトコルの制約としても使えませんし、エクステンションでも使えません。さらに、存在型としても使えないため、ジェネリックスとしては全滅と言えるでしょう。
また、プロトコルにおいても、コピー不可な値型は他のプロトコルに準拠することができません。したがって、プロトコル指向のプログラミングもほぼ全滅です。これを踏まえると、ジェネリックプログラミングやプロトコル指向は現状のSwiftを支える重要な技術であり、ノンコピーアプリの値が使えないという問題は大きいです。
今のところ、ノンコピーアプリの使い道は限られています。例えば、ファイルディスクリプターのような特定のリソースを持って使い回されるリソース管理などが考えられます。しかし、オーナーシップをしっかりと活用するには、Swiftのバージョンがさらに向上する必要があります。もしかすると、Swift 6あたりでこれが改善されるのかもしれません。
現状ではセンダブルプロトコルだけが適用可能です。コンカレンシー関連の機能が使えないのは致命的ですが、幸いにもセンダブルについては問題ありません。ノンコピーアプリとセンダブルプロトコルの組み合わせであれば利用価値はありますが、その他の場面では利用が難しい状況ですので、これからの進展に期待しています。」 時期に確かそれなりに改善されていくはずなので、それは将来に期待ですね。今ちょっとスクロールしながら内容を見ていましたが、このあたりはノンコピーアプリケーションに対して所有権をどう使っていくかについてたっぷり書かれています。今までノンコピーアプリケーションの値の扱い方というのが存在していなかったので、これが新しく搭載されたときに、いろんな所有権がどう動くかというのがしっかり書かれているんです。これを把握することで、今後このあたりが使えるようになってくるのかなと思います。
この部分はすごく長いですね。さて、ジェネリックサポートはどうなるんでしたっけ? 多分下のほうですね。将来的にフィーチャーとしてどこかって話でしたが、ノンコピーアプリタブルやジェネリックサポートなど、将来の目標としていろいろ分かれています。将来を楽しみにするためには、まずコンシューミングなどの基本的な動きを押さえておけば大丈夫かなと思います。
次に、コンシューミングとフォローイングを見ておこうと思います。この辺りは、コンシューミングとフォローイングの二つが搭載されています。インアウトと引き上げてきますが、このフォローイングが値型と使うとほとんど意味を成していないような気がします。コンシューミングは自由に書き換えていい、インアウトは書き換えても値を戻す、という感じですね。コピー不可な値型のときには、どちらかを指定して貸すだけなのか、使うのかを明示することになったようです。その辺も含めて試してみましょう。
では、そもそもなぜ「貸すだけ」とか「消費していいよ」といったあたりが注目されているかというと、インデックス型ぐらいではそこまで使われていなかったですよね。例えば巨大なデータや巨大な配列にしますか。配列で Repeating Array
、なんでもいいですけど、とにかく大きい配列です。これだとコピーオンライトという方針で扱われ、必要なタイミングが来るまではコピーしない裏の最適化が図られています。しかし、普通に作った構造体や重いものなどを渡す際には必ずコピーすると、その都度メモリーをたくさん消費します。大量のメモリ消費は現代ではそこまで問題にならないかもしれませんが、値がコピーされると処理速度が低下し、たくさんループ処理すると処理できなくなる場面もあります。
その際、フォローイングとしてパラメータを渡すと、コピーせずに渡すことができ、最適化してくれるかもしれません。基本的にはコピーしちゃうように見えますが、フォローイングになっていれば最適化が図られる可能性があります。とにかく「渡すだけ」というのと、「インスタンスで戦闘要素だけ取り出す」みたいな場面では、バリューという名前で value2
として、例えば dropFirst
で先頭1個だけ落としたようなものを使ったりします。こういうときにフォローイングで処理しようとする際に、ドロップファーストは自身の要素を編集するので、これも含めて注意が必要です。 定義を見てみると、普通のファンクションにドロップファーストで分けたものが入っているようです。これがエラーになるのは何故でしょうか。フォローイングではないとするとパスするのですが、フォローイングだとエラーが出ていますね。この動きが合っているかどうか疑問です。エラーが出るべき場所に見えませんし、理解が難しいところです。
もしかするとバリューがフォローしてコンシュームできないのかもしれません。編集して書いてみようとしても書けないはずです。こういう動きが正しいのかどうか迷っています。初歩的なことかもしれませんが、これがフォローイングでパスできないのは何故でしょう。
ドロップファーストはミューティングではないので元のものには影響しないで、新しい先頭をカットしたものがリターンされるのです。元の値を影響しないはずなので、コンシュームもしていないように見えます。バリューをコピーして、そのコピーに dropFirst
するのはどうでしょうか。
バリューのコピーは使える演算子で、これを使うとエラーが無くなるかもしれません。コピーすればOKかもしれませんね。コピーしないとエラーが出るのは、値を借りただけでは代入ができないからです。実際、コピーしてあげると借りたものも使えるようになります。
もう一つ考えたのは、ArraySlice
の構造体なのかもしれないということです。dropFirst
がコピーしたインスタンスではなく、元の Array
のどこを削除しているかという情報を持っているだけなので、バッファーは共有している可能性もあります。エラーが出る原因がこの辺りかもしれません。
再現するには、例えば以下のようにします。
var buffer = [1, 2, 3, 4, 5]
var value = buffer.dropFirst()
これでプリントしてみると、エラーが出ずに動くかもしれません。しかし、dropFirst
が直接働いていない気がしたら次の方法も試してください。値型と構造体(参照型)で動きが変わっているかもしれません。
var value = [1, 2, 3, 4, 5]
value = value.dropFirst()
print(value)
これはバリュー型にして、フォローイングで処理する形にし、プリントして確認します。また、以前の勉強会で試したときにはエラーにならなかったはずですが、コピーはあまり関係なく、通用するようでした。
このように、コンシューミングとコピーについて考えると、基本的にコピーした方が良い場合があります。前回の勉強会でもこの部分にエラーは出なかったはずです。こうした基本的な部分をもう一度確かめてみると良いでしょう。 アンドロイドではそのような処理が図られるのか疑問に思いますが、基本的に消費可能な値型はカスタムストリングコンバーティブルで利用できます。消費可能な関数に渡しても、コピー可能な値ですので、コピーした後のものが中で再利用されるのです。
例えば、以下のように消費可能な値型を渡します。
var value = 0
print(value)
このコードでは、value
が消費され、value
が0に設定されているため、print
で出力すると0が表示されます。この仕組みは、ボローイング(借用)の概念と関連があります。
ボローイングを用いると、その変数が書き換えられません。しかし、ボローイングによって消費されない場合、その変数は再度使用可能です。たとえば、以下のコードでは、n
がボローイングに渡され、再度使用可能です。
let n = 10
print(n) // これは動作します
ボローイングの場合、その変数を他の関数に渡すことができますが、消費する場合にはエラーが発生します。以下に示すように、アブシンクバリューとして受け取れる形です。
func consume<Value>(_ value: Value) { /*...*/ }
consume(n) // ボローイングではなく消費
コピー不可能な値型の場合、そのまま渡せず、明確に書く必要があります。ノンコピーアブルの場合、カスタムストリングコンバーティブルとしても書けないことがあります。
struct NonCopyable {
let value: Int
// NonCopyableをカスタムストリングコンバーティブルとして書けない場合あり
}
ノンコピーアブルである場合、どう受け取るかを厳密に指定し、最適化を図ることができます。コピー可能な値はコピー渡しとなり、ボローイングや消費する際にはエラーを防ぐための対策が必要です。
このように、デフォルトの渡し方に関しての情報はドキュメントに明記されていますので、それを事前に把握しておくことが重要です。後で詳しく調べてみることにしましょう。
次回は、今日の内容を整理し、コンシューミングとボローイングに関する詳細をもう少し深掘りして紹介できればと思っています。
今日はこれで終わりにします。お疲れ様でした。