ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftでのコピー不可な型の使用
Swiftでコピー不可な型を使用する方法について説明します。Swiftでのコピーの意味、コピー不可な型を使用するケース、値の所有権によってデベロッパの意図を明確に示す方法を学びましょう。
関連する章
- 0:00 - Introduction
- 0:30 - Agenda
- 0:50 - Copying
- 7:05 - Noncopyable Types
- 11:50 - Generics
- 19:12 - Extensions
- 21:24 - Wrap up
リソース
- Copyable
- Forum: Programming Languages
- Swift Evolution: Borrowing and consuming pattern matching for noncopyable types
- Swift Evolution: Noncopyable Generics
- Swift Evolution: Noncopyable Standard Library Primitives
関連ビデオ
WWDC19
-
ダウンロード
こんにちは SwiftチームのKavonです 「Swiftでのコピー不可な型の使用」 へようこそ 私たちは一人ひとりが唯一の存在です しかしSwiftの値はそうではありません 値はコピーできるからです 値が一意であると保証することは プログラミングにおいて 強力な概念となります 嬉しいことに このたび Swiftにコピー不可な型を 導入しました 本日は多くのエキサイティングな内容を カバーします まず コピーの仕組みについて復習します 次に 所有権と コピー不可な型についてお話しし 最後に コピー不可な型の ジェネリックな使用や これらの型のExtensionの記述など 高度なトピックについて説明します それでは始めましょう 新しいゲームを開発しています 絵文字をアイコンとして表す Player型を定義しました 2人のプレーヤーを作成しましょう 現時点でこれらは同じです 直感的に 一方のアイコンを変えても もう一方の プレーヤーに影響しないことは分かります その仕組みについて 順を追って見ていきましょう 最初の行でplayer1を カエルとして指定します この変数の内容は Playerを構成する実際のデータです これが値型という構造体だからです player2に移ります player1と同じにします どういう意味でしょうか player1のコピーを作成するという意味です 変数をコピーすると その内容もコピーすることになります player2のアイコンを変更すると player1とは独立したPlayerを 変更していることになります 値のコピーや破棄について 考える必要すらありませんでした Swiftが対応してくれるためです
では Playerが参照型であった場合は どうでしょうか それは単に 構造体からクラスに 変更したということです 先ほどと同じコードで 何が起こるでしょうか 最初のステートメントを分析してみましょう
PlayerClassを構築する際 データを格納するための オブジェクトが個別に割り当てられます
player1の内容は そのオブジェクトに対する 自動管理参照となります これが参照型であることの意味です 次のステートメントでは player2はplayer1と同じです つまり参照がコピーされるだけで オブジェクト自体が コピーされるわけではありません これはシャローコピーとも呼ばれる 非常に高速なコピーです 両方のプレーヤーが 同じオブジェクトを参照するため アイコンを変更すると 両方のアイコンが変わります そのため このアサーションは 成り立ちません どちらの場合もコピーの 動作は同じでした 唯一違う点は 値をコピーするか 参照をコピーするかという点です イニシャライザを定義して ディープコピーを行うことで 参照型を値型のように 動作させることができます このイニシャライザは一例です オブジェクトと そのオブジェクトが指す すべての要素を再帰的に再作成します
これを行うには Iconの イニシャライザを呼び出し 別のPlayerの アイコンを使って再作成します これにより 新しいプレーヤーは 他のプレーヤーと参照を共有しません
先ほどのプログラムで player2をplayer1と 同じにした状態に戻り ディープコピーで動作が どのように変わるか確認しましょう
現時点では 両方のプレーヤーが 同じオブジェクトを参照しています
プレーヤーのフィールドに記述する前に そのイニシャライザを呼び出して ディープコピーを作成します
これにより 同一であるものの 個別のオブジェクトが割り当てられ 変更によって他の変数が 影響を受けることがなくなります
これがコピーオンライト方式の本質です 変更しても依存しないため 値型と同じ動作が得られます
新しい型を設計する場合 その値をディープコピーできるかどうかは すでに制御できていました これまで制御できなかったのは Swiftで自動的にコピーを 作成できるかどうかです Copyableは 型を自動コピーできるかどうかを記述する 新しいプロトコルです Sendableと同様に メンバー要件はありません
デフォルトでは Swiftですべての要素が Copyableであると推論されます すべての要素です すべての型が自動的に Copyableに適合しようとします どのジェネリック型のパラメータでも 入力する型は自動的に Copyableになります すべてのプロトコルや関連する型も 具象型が自動的に Copyableに適合し
すべてのボックス化されたプロトコル型も 自動的にCopyableを含めて構成されます
先ほど紹介した通り Copyableと 記述する必要はありません 記述されていなくても そのように設定されています コピーの機能が必要であると Swiftで想定されている理由は Copyable型を扱う方が はるかに簡単であるためです
ただし コピーによってコードにエラーが 発生しやすくなる状況もあります 例えば バックエンドの銀行振替を モデル化するための型を記述するとします 実際の振替は 保留中 キャンセル済み 完了のいずれかです
この型には振替を完了する runメソッドを指定します これが振替のスケジュールを 設定するための関数です 誤って振替を2回行ったら ユーザーの不満の原因になるため もう一度確認しましょう 遅延が1秒未満であれば すぐに実行します しかし 戻るのを忘れているため 失敗して再度実行することになります このようなコストがかかる単純ミスには どう対処すればよいでしょうか ここで 振替の状態を追跡する 変数を追加して アサーションで再実行の試みが 検出されるようにします ただし バグの検出テストを記述しない限り アサーションでバグは見つけられないため バックエンドサービスをダウンさせるような バグが依然としてあります 実は このスケジュール関数にも 別のバグがあります スリープ中のタスクがキャンセルされたら どうなるか考えてみましょう スローされたエラーを 呼び出し元が慎重にチェックしないと 振替をキャンセルし忘れることがあります
キャンセルを忘れず行うためのdeinitを BankTransferに追加する方法もあります しかしこれは実用的ではありません
startPayment関数を詳しく 見てみましょう 振替がスケジュールされたことの追跡用に 振替のコピーを保持しています これは問題です すべてのコピーを破棄しないと 振替のdeinitが実行されないためです この問題の根本原因は プログラム内にある この振替のコピー数を 制御できていない点です 多くの場合 値をコピーする機能は 型のデフォルトとして適切ですが 型をコピー不可にした方が 適切な場合もあります BankTransferの問題は 一旦置いておいて コピー不可な型について説明します
FloppyDiskをモデル化するとします このように記述すると デフォルトで Copyableに適合性がある型になります ただし 適合性を宣言する部分で Copyableという単語の前に ティルダを記述すると Copyableに対するデフォルトの 適合性を抑止できます これでFloppyDiskはCopyableに まったく適合しなくなります ティルダの付いたCopyableは その型のCopyableをなくすための宣言だと 考えてみます そのフロッピーをコピーしようとすると どうなるでしょうか コピーはサポートされないため 代わりにSwiftで使用されます
consumeと記述して 明示的に指定できますが 指定しなくてもそうなります
変数が使用されると値が取られ その変数は初期化されないまま残ります
そのため 使用する前に システムディスクだけが初期化されます
使用することで システムディスクのコンテンツが バックアップディスクに移動されます 後でシステムディスクを読み込むと 何も入っていないためエラーになります
次に 新しいディスクを作成する 関数について考えてみましょう formatを呼び出す場合 変数resultはどうなるでしょうか 関数のシグネチャには ディスクに渡す必要のある 所有権が宣言されていないため どうなるかはわかりません コピー可能なパラメータの場合 この点を考慮する必要はありませんでした formatはディスクのコピーを 事実上受け取ることになります ただし コピー不可なパラメータでは コピーできないため 関数が値に対して保持する所有権を 宣言する必要があります
1つ目の種類の所有権は consumingと呼ばれるものです 関数によって呼び出し元から 引数が移動されることを意味します 自分のものになるため 変更することもできます
ただし ここで引数を使用すると 問題が発生します formatではディスクに 何も返されないためです よく考えてみると ディスクをフォーマットする場合 一時的にアクセスするだけで済みます
一時的にアクセスする場合 それを借りることになります borrowingでは letバインディングのように 引数への読み取りアクセスが付与されます
内部では ほぼすべてのパラメータやメソッドが Copyable型に対してこのように機能します
違いは 明示的に借りた引数は 使用または変更できないという点です コピーしかできません このformat関数では最終的に ディスクに変更を加える必要があるため borrowingも使えません
最後の種類の所有権は 皆さんもご存知のinoutです これはメソッドにおける mutatingの記述と同じものです
Inoutでは 呼び出し元にある変数への 一時的な書き込みアクセスを許可します 書き込み権限があるため パラメータを使用できます ただし 関数が終了する前の ある時点で inoutパラメータの再初期化が必要です 呼び出し元は復帰時に値があることを 想定しているためです では BankTransferの例に戻りましょう 使用可能なリソースとして モデル化できるようになったためです まず BankTransferをコピー不可な 構造体として指定し runメソッドをconsumingとマークしました これにより selfの値が 呼び出し元から移動されます この2つの変更だけで アサーションも不要になりました Swiftで 同じ振替に対してrunメソッドを 2回呼び出すことができなくなりました 所有権は正確に追跡されるため この構造体にdeinitを追加すれば 破棄された場合に runではなく アクションがトリガされます runメソッドが終了すると 自動的にselfが破棄されるため discard selfと記述します これでdeinitを呼び出すことなく selfが破棄されます schedule関数のバグが どうなるか見てみましょう まず transferパラメータに 所有権を追加する必要があります scheduleは最後の使用を意図しているため consumingが理にかなっています コンパイルしようとすると Swiftでバグが検出されています ifステートメントがフォールスルーするため transferが2回 使用される可能性があります それを防ぐためreturnを追加します では もう一つのバグはどうでしょうか これも定義済みです scheduleがtransferの 最後の所有者であるため sleepが投げられるとdeinitが実行され 振替がキャンセルされます
コピー不可な型はプログラムの正確性を 高める上で優れたツールになります ジェネリックコードを含め あらゆる場所で使いたいと思うでしょう Swift 6では コピー不可な ジェネリクスを使うことができます これは新しいジェネリクスシステム ということではなく Swiftの既存のジェネリクスモデルを ベースとしています ここでジェネリクスについて 再確認しましょう
まず Swiftにある 型の世界について考えてみましょう すべての型がうまく共存しています Stringも Commandと呼ばれる独自の型もあります
プロトコルRunnableはこの世界の 部分空間を定義し 適合する型が含まれています 現時点では適合するものがないため 空になっています もしRunnableに適合するように Commandを拡張すると この点はRunnable空間に移動します ジェネリクスの核となる考え方は 適合制約がジェネリック型を 記述するということです executeと呼ばれるジェネリック関数で 考えてみましょう 山括弧の中に入っているTは この世界に存在する型を表す 新しいジェネリック型 パラメータを宣言していますが どの型かはわかりません Copyableはあらゆる場所に 適用されていると説明しましたが このTにはデフォルトで制約があります 入力型がCopyableに 適合している必要があるのです
Commandはデフォルトで Copyableに適合しており RunnableもCopyableを継承しています 最近まで 明示されてはいないものの Swiftにあるすべての型がCopyableでした
つまりこの空間に含まれるすべての型を execute関数に渡すことができます 唯一の制約はTが Copyableに適合することだからです
CommandはRunnableにも 適合していますが より広範なCopyableの空間にも 依然として存在しており 特定の型がRunnableである場合も そうでない場合もあります このジェネリックパラメータTでは 追加の適合性を持つ型は除外していません 前述の通り execute関数では Copyable以外の適合性は必要ありません
ただし execute関数を実装するために TをRunnableにして runメソッドを呼び出す必要があります そこで where句を使って TにRunnable制約を追加します
これにより Tに許容される型の空間に さらに制約を設けたことになります RunnableとCopyableの組み合わせで 空間が絞り込まれています Commandは含まれていますが Stringは除外されました StringにはRunnableに対する 適合性がないためです
Swift 5.9以来 この世界は拡大し続けています Copyableに適合しない型も 存在するためです 例えば 新しく登場した BankTransfer型は適合しないため 点は外側にあります Copyableの適合性を抑止するには ティルダ付きのCopyableを記述するしかなく これを より広範な空間と呼んでいます
なじみのあるSwiftの型のほとんどが Copyableに適合しています では それらはティルダ付きのCopyable内に どのように含まれているのでしょうか
先ほどと同様 より広範なこの空間では Copyableに適合する特定の型を 想定することはできません Copyableである場合も そうでない場合も考えられるとして ティルダ付きのCopyableを 理解する必要があります
Any型はどうでしょうか これは常にCopyableであり そうあるべきです ほぼすべてのプログラミング言語では どの型もCopyableです
これでコピー不可な ジェネリクスについて説明できます 先ほどのRunnableプロトコルから 見ていきましょう
型の世界は現在 このようになっています Runnableの値はすべてCopyableです
BankTransferはCopyableではないため Runnableにも含まれません ただし BankTransferも適合させたいため ジェネリクスとして使用します Runnable型をコピーする機能は プロトコルにとって不可欠ではないため ティルダ付きのCopyableを追加して RunnableからCopyable制約を削除します
階層が変わり Copyable空間がRunnableを含むのではなく 重なるだけになります CommandはRunnableかつCopyableとなり 重なり合う部分に収まります 次に Runnableの適合性で BankTransferを拡張すると この点はCopyableとは重なり合わない Runnableの部分に移動します
ジェネリック関数executeに 話を戻しましょう
ジェネリックパラメータTには まだデフォルトの制約があるため RunnableかつCopyableである Commandのような型だけが executeで許可されています
ティルダ付きのCopyableを使って TからCopyable制約を削除します
制約を取り除くことで 許可される型は Runnableであるすべての型に広がります execute関数でTはCopyableでない 可能性があることを示唆しています これが重要なポイントです 通常の制約は 許可される型を絞り込んで 具体的に指定しますが ティルダ制約は具体的な指定を 緩和させることで幅を広げます ではこれらの理論を 実践してみましょう Runnable型を Jobという新しい構造体に 含めたいと思います ジェネリックパラメータActionを Jobに定義しています ActionはRunnableですが Copyableでない可能性があります このままの記述ではエラーが 発生してしまいます Job構造体はデフォルトで Copyableに適合しているため Copyableのデータのみを含められます コピー不可な値を別の値に 格納する方法は2通りあります クラスをコピーしても参照だけが コピーされるようクラス内に格納する方法と 格納されている型自体のCopyableを 抑止する方法です ここでは2つ目の方法を使い Jobをコピー不可にします
この場合も依然としてActionに Command型を指定できます ActionはCopyable型が表示されるのを 阻止しないためです JobではActionを コピーする必要がないため コピー不可な型も機能します ただし Actionに指定した型が Copyableである場合はどうなるでしょうか Jobはアクションを入れるだけの 入れ物なのでコピーできます
Jobを条件付きでCopyableと宣言することで API作成者としてコピーを許可できます Extensionでは ActionがCopyableの場合に JobもCopyableになると宣言されています
型の世界でどう見えるかと言うと
Actionの具体的な型に指定するまで JobがCopyableかどうかわかりません では Commandを使いましょう
CommandはCopyableであるため JobのCommandもCopyableです
ActionをBankTransferに変えると 適合性条件が満たされないため JobのBankTransferは Copyableになりません
コピー不可なジェネリクスの 全体的な考え方としては デフォルトのCopyable制約を 除去するということです コピー不可なジェネリックパラメータで 型を定義する方法を説明しました この型のExtensionについて 詳しく見ていきましょう Actionのgetterメソッドを 定義するとします
Jobの通常のExtensionを使って 追加します
呼び出すのは問題ありませんが これはActionのコピーでしょうか そうです アクションを返す際にコピーされます これはExtensionのエラーではありません
このプレーンなExtensionは デフォルトでJobに制約されており そのActionはCopyableであるためです
つまりこのgetterは正常です BankTransferジョブの場合は 呼び出し可能でないためです Extensionの仕組みとしては Extension型の範囲内にある ジェネリックパラメータはCopyableに制約され プロトコルのSelfも該当します
このようにExtensionを動作させるのは 優れたメリットがあります Jobが自分で記述していない JobKitモジュールの一部であるとします Cancellable型を記述する プロトコルがここにあります コピー不可な型が何かわからないものの とにかくJobを適合させたいとします このExtensionを記述すれば 機能するため問題ありません
適合性には Actionが Copyableであるという条件が デフォルトで付いているためです 一般にActionはCopyableでないため ActionがCopyableとなる場合 Jobも CopyableとなりCancellableに適合します
このJob型を公開することで Copyable型だけを扱うプログラマが これを使うことができます では Copyableを問わずこのExtensionを すべてのジョブに適用したい場合は どうすればよいでしょうか
このExtensionのActionから Copyable制約を取り除くだけで ActionがCopyableかどうかを想定せずに JobがCancellableに適合されます
本日は Swiftにおけるコピーの仕組みと コピーで生まれる課題について見てきました 所有権について考える必要はありますが コピー不可な型は プログラムの正確性を高める上で 有用なツールとなります 紹介したステップによって Optional UnsafePointer Resultを使い コピー不可なジェネリクスを 標準ライブラリに追加できるようになります 詳しくはSwift Evolutionの 提案事項をお読みください コピー不可なジェネリクスや borrowingとconsumingのパターンマッチング コピー不可な標準ライブラリの プリミティブについて確認できます 「Swift Programming Language」ブックでも 詳しく学ぶことができます もっと全般的に コピーオンライト方式や ジェネリック型を設計する際の ベストプラクティスについて学びたい方は WWDC 2019の 「Modern Swift API Design」をご覧ください ご視聴ありがとうございました WWDCをお楽しみください
-
-
0:52 - Player as a struct
struct Player { var icon: String } func test() { let player1 = Player(icon: "🐸") var player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
1:55 - Player as a class
class PlayerClass { var icon: String init(_ icon: String) { self.icon = icon } } func test() { let player1 = PlayerClass("🐸") let player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
3:00 - Deeply copying a PlayerClass
class PlayerClass { var data: Icon init(_ icon: String) { self.data = Icon(icon) } init(from other: PlayerClass) { self.data = Icon(from: other.data) } } func test() { let player1 = PlayerClass("🐸") var player2 = player1 player2 = PlayerClass(from: player2) player2.data.icon = "🚚" assert(player1.data.icon == "🐸") } struct Icon { var icon: String init(_ icon: String) { self.icon = icon } init(from other: Icon) { self.icon = other.icon } }
-
5:10 - Copyable BankTransfer
class BankTransfer { var complete = false func run() { assert(!complete) // .. do it .. complete = true } deinit { if !complete { cancel() } } func cancel() { /* ... */ } } func schedule(_ transfer: BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() } try await Task.sleep(for: delay) transfer.run() } func startPayment() async { let payment = BankTransfer() log.append(payment) try? await schedule(payment, .seconds(3)) } let log = Log() final class Log: Sendable { func append(_ transfer: BankTransfer) { /* ... */ } }
-
7:46 - Copying FloppyDisk
struct FloppyDisk: ~Copyable {} func copyFloppy() { let system = FloppyDisk() let backup = consume system load(system) // ... } func load(_ disk: borrowing FloppyDisk) {}
-
8:18 - Missing ownership for FloppyDisk
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: FloppyDisk) { // ... }
-
9:00 - Consuming ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: consuming FloppyDisk) { // ... }
-
9:26 - Borrowing ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: borrowing FloppyDisk) { var tempDisk = disk // ... }
-
9:55 - Inout ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { var result = FloppyDisk() format(&result) return result } func format(_ disk: inout FloppyDisk) { var tempDisk = disk // ... disk = tempDisk }
-
10:28 - Noncopyable BankTransfer
struct BankTransfer: ~Copyable { consuming func run() { // .. do it .. discard self } deinit { cancel() } consuming func cancel() { // .. do the cancellation .. discard self } }
-
11:10 - Schedule function for noncopyable BankTransfer
func schedule(_ transfer: consuming BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() return } try await Task.sleep(for: delay) transfer.run() }
-
12:12 - Overview of conformance constraints
struct Command { } protocol Runnable { consuming func run() } extension Command: Runnable { func run() { /* ... */ } } func execute1<T>(_ t: T) {} func execute2<T>(_ t: T) where T: Runnable { t.run() } func test(_ cmd: Command, _ str: String) { execute1(cmd) execute1(str) execute2(cmd) execute2(str) // expected error: 'execute2' requires that 'String' conform to 'Runnable' }
-
15:50 - Noncopyable generics: 'execute' function
protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } } func execute2<T>(_ t: T) where T: Runnable { t.run() } func execute3<T>(_ t: consuming T) where T: Runnable, T: ~Copyable { t.run() } func test() { execute2(Command()) execute2(BankTransfer()) // expected error: 'execute2' requires that 'BankTransfer' conform to 'Copyable' execute3(Command()) execute3(BankTransfer()) }
-
18:05 - Conditionally Copyable
struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } func runEndlessly(_ job: consuming Job<Command>) { while true { let current = copy job current.action?.run() } } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } }
-
19:27 - Extensions of types with noncopyable generic parameters
extension Job { func getAction() -> Action? { return action } } func inspectCmd(_ cmdJob: Job<Command>) { let _ = cmdJob.getAction() let _ = cmdJob.getAction() } func inspectXfer(_ transferJob: borrowing Job<BankTransfer>) { let _ = transferJob.getAction() // expected error: method 'getAction' requires that 'BankTransfer' conform to 'Copyable' } struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } }
-
20:14 - Cancellable for Jobs with Copyable actions
protocol Cancellable { mutating func cancel() } extension Job: Cancellable { mutating func cancel() { action = nil } }
-
21:00 - Cancellable for all Jobs
protocol Cancellable: ~Copyable { mutating func cancel() } extension Job: Cancellable where Action: ~Copyable { mutating func cancel() { action = nil } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。