🔐

Swift 5.9の新機能:`~Copyable`と所有権について

2023/09/12に公開

Swift 5.9には、多くの新しい機能や改善が盛り込まれていますが、注目の変更としてnoncopyable("move-only"タイプとも呼ばれます)の導入があります。これは、変数やオブジェクトが一意の所有権を持つことを意味し、従来のSwiftの型とは異なり、自由にコピーすることができない新しい型のことを指します。

従来のコピー可能な構造体や列挙型は、一意のリソースを表現するのに最適ではありませんでした。

classの場合、classが参照型であるため、オブジェクトは初期化されると一意のアイデンティティ(参照)持ちます。これにより、一意のリソースを表現することが可能となります。

class UniqueResource {
  var uniqueName: String
    
  init(uniqueName: String) {
      self.uniqueName = uniqueName
  }
}

var resource1 = UniqueResource(uniqueName: "unique")
var resource2 = resource1

この例では、resource1resource2は同じクラスのインスタンスを参照していますが、そのことがヒープ上でのオブジェクトのライフサイクルの管理や、リソースのアクセスを調整するための追加のオーバーヘッドを発生させる問題点となります。

コピー不可能な構造体や列挙型の概念が導入

Swift5.8以前のバージョンでは構造体や列挙型はコピー可能でしたが、SE-0390では、コピー不可能な構造体や列挙型の概念が導入されます。
これにより、コピー不可能な型の値は、常に一意な所有権を持ち、その値はコピーすることができなくなります。
さらに、この型の値は一意のアイデンティティを持つので、structに対してdeinitの実装が可能となります。

Copyableの制約とは?

Swiftにおいて型のコピーは、これまでのバージョンではほとんどが暗黙のうちに行われていました。このことは、ある意味でSwiftの多くの型がどれもコピー可能であったことを示しています。
しかし、Swift 5.9では、型がコピーが可能であることを明示するための新しい制約Copyableが導入されます。

Swiftの標準ライブラリに、Copyable制約が含まれるようになり、これにより、すでに存在するほとんどの主要な型はこの制約を暗黙的に満たすようになり、GenericsProtocolなどにもこの制約を暗黙的に必要としています。

しかし、Xcode 15.0 beta8の段階では、開発者が型にCopyableを明示的に指定する機能はまだサポートされておらず、Copyableを指定するとコンパイルエラーが発生します。

struct MyValue: Copyable { // ERROR: Cannot find type 'Copyable' in scope
  var value: Int = 0
}

コピー不可能(~Copyable)な型の宣言方法

Swift 5.9のCopyableの制約導入にともない、その反対の概念として~Copyableも導入されます。~Copyableは、型がコピー不可能であることを明示するためのものです。これにより、型のインスタンスは一意の所有者のみが持つことができ、そのインスタンスのコピーは許可されなくなります。

~Copyableを使用して、コピー不可能な型を宣言する方法は以下のようになります。

struct MyValue: ~Copyable {
  var value: Int = 0
}

この~は否定を意味します。したがって、~CopyableCopyableではない、つまりコピー不可能であることを示しています。

~Copyableプロパティを持つ構造体がある場合、その上位の型も~Copyableとして宣言する必要があります。

struct MyValue: ~Copyable {
  var value: Int = 0
}

struct YourValue { // ERROR: Struct 'YourValue' cannot contain a noncopyable type without also being noncopyable
  var value: Int = 0
  var myValue = MyValue()
}

~Copyablestructは一意のアイデンティティを持つため、これにより、structでもclassと同様にdeinitを使用することができます。

struct MyValue: ~Copyable {
  var value: Int = 0

  deinit { // deinit を使用することができる
    print("deinit:", value) 
  } 
}

所有権の移動: ~Copyableの挙動を深掘り

前のセクションでは、~Copyableを利用したコピー不可能な型の宣言方法を紹介しました。では、~Copyableで宣言された型は具体的にどのような挙動を示すのでしょうか?特に、~Copyableという制約によって強調されている「所有権(ownership)」という概念は、Swiftのプログラムの安全性と効率性を向上させるための鍵となる要素です。

所有権の移動とは、一度変数や定数に値が代入された後、その変数や定数を他の変数や定数に代入することで、最初の変数や定数の使用権限が無効となることを指します。以下の例を参照してください。

まず、MyValueのインスタンスをmyValueに代入します。この時点でmyValueはそのインスタンスの所有権を持っています。しかし、myValueyourValueに代入すると、myValueの所有権は失われ(myValueconsume、つまり消費されます)、その後myValueを利用することはできません。そのため、myValue.debugValue()を利用するとコンパイルエラーが発生します。

struct MyValue: ~Copyable {
  var value: Int = 0
  
  func debugValue() {
    print(value)
  }
}

func main() {
  let myValue = MyValue() // ERROR: 'myValue' used after consume
  let yourValue = myValue

  myValue.debugValue()
}

ただし、所有権の移動は連鎖的に行うことができます。そのため、以下のように一連の代入を行うことは問題ありません。もちろん、各変数は代入後にその所有権を失うので、後続のコードでの利用は不可能となります。

func main() {
  let myValue = MyValue()
  let yourValue = myValue
  let thisValue = yourValue
  let thatValue = thisValue // ✅: コンパイルエラーは発生しない
}

このような所有権の概念は、リソースの管理やメモリ安全性を強化する上で非常に重要となります。

SILレベルで変数のライフサイクルがどのように変化するかを見る

SILSwift Intermediate Language)はSwiftのコードがLLVMの低レベルIRに変換される前の中間表現です。このSILを通じて、変数のライフサイクルがどのように変化するのかを見てみます。

サンプルコード

struct MyValue: ~Copyable {
  var value: Int = 0

  init() { print("init:", value) }
  deinit { print("deinit:", value) }

  func debugValue() {
    print(value)
  }
}

func main() {
  let myValue = MyValue()
  let yourValue = myValue
}

main()

上記のコードに対するSILは、コマンドswiftc -emit-sil main.swift -o main.silを使って生成しました。
以下に生成したSILの一部を示します。

SILの一部抜粋

// main()
sil hidden @$s4mainAAyyF : $@convention(thin) () -> () {
bb0:
  %0 = alloc_stack [lexical] $MyValue, let, name "myValue" // users: %6, %4, %15
  %1 = metatype $@thin MyValue.Type               // user: %3
  // function_ref MyValue.init()
  %2 = function_ref @$s4main7MyValueVACycfC : $@convention(method) (@thin MyValue.Type) -> @owned MyValue // user: %3
  %3 = apply %2(%1) : $@convention(method) (@thin MyValue.Type) -> @owned MyValue // user: %4
  store %3 to %0 : $*MyValue                      // id: %4
  %5 = alloc_stack [lexical] $MyValue, let, name "yourValue" // users: %9, %8, %13, %14
  %6 = load %0 : $*MyValue                        // user: %8
  debug_value undef : $*MyValue, let, name "myValue" // id: %7
  store %6 to %5 : $*MyValue                      // id: %8
  %9 = load %5 : $*MyValue                        // user: %11
  // function_ref MyValue.debugValue()
  %10 = function_ref @$s4main7MyValueV05debugC0yyF : $@convention(method) (@guaranteed MyValue) -> () // user: %11
  %11 = apply %10(%9) : $@convention(method) (@guaranteed MyValue) -> ()
  debug_value undef : $*MyValue, let, name "yourValue" // id: %12
  destroy_addr %5 : $*MyValue                     // id: %13
  dealloc_stack %5 : $*MyValue                    // id: %14
  dealloc_stack %0 : $*MyValue                    // id: %15
  %16 = tuple ()                                  // user: %17
  return %16 : $()                                // id: %17
} // end sil function '$s4mainAAyyF'

ライフサイクルについて

SILの動きを追うと、以下のプロセスが確認できます。

  • alloc_stack命令によりmyValueのためのスタックメモリが確保される。
  • 新しいMyValueインスタンスが生成され、上記のスタックメモリに保存される。

次に、yourValueのために再びalloc_stack命令を使用してスタックメモリが確保されます。そして、myValueの内容がyourValueのメモリ領域にコピーされます。このプロセスは、以下のloadstoreの命令によって示されています。

%6 = load %0 : $*MyValue                        // user: %8
store %6 to %5 : $*MyValue       

debugValue関数が呼ばれてyourValueの内容が表示されるものの、この関数の内部でのスタックの状態に変化はありません。

関数の終わりでは、yourValueのメモリ領域が最初に解放され、続いてmyValueのメモリ領域も解放されます。

destroy_addr %5 : $*MyValue                     // id: %13
dealloc_stack %5 : $*MyValue                    // id: %14
dealloc_stack %0 : $*MyValue                    // id: %15

LIFO(Last-In-First-Out、後入れ先出し)の原則に従い、最後に確保したyourValueの領域が最初に解放されます。

@_moveOnlyについて

MyValueの構造体はSILでの表現において、~Copyableではなく@_moveOnlyが指定されています。元のコードから~Copyableを取り除き、代わりに@_moveOnlyを追加することで、コンパイルが成功し、値のコピーも防ぐことができました。

@_moveOnly struct MyValue {
  @_hasStorage @_hasInitialValue var value: Int { get set }
  init()
  deinit
  func debugValue()
}

~Copyableなパラメータを引数として持つメソッドの宣言について

関数やメソッドの引数として~Copyableを使用する際、inout, borrowing, consumingのいずれかのキーワードを指定する必要があります。指定しないと、コンパイルエラーが発生します。

struct MyValue: ~Copyable {
  var value: Int = 0

  init() { print("init:", value) }
  deinit { print("deinit:", value) }
}

func doSomething(myValue: MyValue) { // ERROR: Noncopyable parameter must specify its ownership
}

func main() {
  let myValue = MyValue()
  doSomething(myValue: myValue)
}

では、実際にborrowingconsumingを指定した場合にどのような変化が発生するかを見ていきます。

borrowingを指定する場合

borrowingを指定する場合、引数のmyValueimmutableとなります。そのため、メソッドでmyValueの値を変更することはできません。

func doSomething(myValue: borrowing MyValue) {
  myValue.value = 2 // ERROR: Cannot assign to property: 'myValue' is a 'let' constant
}

func main() {
  let myValue = MyValue()
  doSomething(myValue: myValue)
}

所有権自体はメソッドの呼び出し元(Caller)が保持しています。呼び出し先(Callee)は所有権を一時的に借りるだけです。ゆえに、doSomethingを呼び出した後も、myValueを利用することは可能です。

func main() {
  let myValue = MyValue()
  doSomething(myValue: myValue)

  myValue.debugPrint() // ✅ myValueが所有権持っているのでコンパイルエラーにならない
}

consumingを指定する場合

consumingを指定する場合、そのメソッド内でmyValueは消費され、スコープを抜ける際にmyValueのライフサイクルは終了します。

ライフサイクルが終了するという約束があるので、メソッドのスコープ内でvalueの値を変更することが可能になります。これはborrowingを指定する場合とは異なる点です。

func doSomething(myValue: consuming MyValue) {
  myValue.value = 9 // ✅ valueの値を変更できる。
}

func main() {
  let myValue = MyValue()
  doSomething(myValue: myValue)
}

このメソッドを呼び出す場合、呼び出し元(Caller)にあった所有権は失われ、代わりに呼び出し先(Callee)に所有権が移動します。そのため、doSomethingを呼び出した後にmyValueを利用するとコンパイルエラーが発生します。

func main() {
  let myValue = MyValue() // ERROR: 'myValue' used after consume
  doSomething(myValue: myValue)

  myValue.debugValue()
}

ライフサイクルの終了を確認する

先ほど、consumingが指定された場合、メソッドを抜けるタイミングでオブジェクトのライフサイクルが終了することを紹介しました。しかし、この挙動は具体的にどのように実現されているのでしょうか? borrowingconsumingの挙動の違いを、ディスアセンブリを用いて確認してみましょう。

サンプルコード

func doSomething1(myValue: borrowing MyValue) {
  return
}

func doSomething2(myValue: consuming MyValue) {
  return
}

以下の手順で、doSomething1doSomething2のディスアセンブリを実行します:

  1. swiftc main.swift -o main
  2. lldb main
  3. disassemble --name doSomething1
  4. disassemble --name doSomething2

結果は以下の通りです:

doSomething1(borrowing)のディスアセンブリ結果

(lldb) disassemble --name doSomething1
main`main.doSomething1(myValue: main.MyValue) -> ():
main[0x100003a48] <+0>:  sub    sp, sp, #0x10
main[0x100003a4c] <+4>:  str    xzr, [sp, #0x8]
main[0x100003a50] <+8>:  str    x0, [sp, #0x8]
main[0x100003a54] <+12>: add    sp, sp, #0x10
main[0x100003a58] <+16>: ret    

doSomething2(consuming)のディスアセンブリ結果

(lldb) disassemble --name doSomething2
main`main.doSomething2(myValue: __owned main.MyValue) -> ():
main[0x100003a5c] <+0>:  sub    sp, sp, #0x20
main[0x100003a60] <+4>:  stp    x29, x30, [sp, #0x10]
main[0x100003a64] <+8>:  add    x29, sp, #0x10
main[0x100003a68] <+12>: str    xzr, [sp, #0x8]
main[0x100003a6c] <+16>: str    x0, [sp, #0x8]
main[0x100003a70] <+20>: ldr    x0, [sp, #0x8]
main[0x100003a74] <+24>: bl     0x1000038b4               ; main.MyValue.deinit
main[0x100003a78] <+28>: ldp    x29, x30, [sp, #0x10]
main[0x100003a7c] <+32>: add    sp, sp, #0x20
main[0x100003a80] <+36>: ret  

これらのディスアセンブリ結果から、以下の点に着目することができます

  1. consumingを使用したdoSomething2では、メソッドを抜ける直前にmain.MyValue.deinitが呼び出されています。これは、myValueのライフサイクルがメソッドの終了時点で終了していることを示しています。
  2. 一方、borrowingを使用したdoSomething1では、deinitは呼び出されていません。これは、オブジェクトのライフサイクルが継続していることを示しています。

この比較から、consumingborrowingを利用した場合のライフサイクルの違いを確認することができます。

~Copyableな型のメソッドについて

~Copyableな型のメソッドには、borrowing, consuming, mutatingのいずれかのキーワードを付与します。borrowingはデフォルト値となっているため、これらのキーワードを明示的に付与しない場合、borrowingとして扱われます。

このため、~Copyableな型のメソッドにborrowingを明示的に付与する必要はありません。

struct MyValue: ~Copyable {
  var value: Int = 0

  init() { print("init:", value) }
  deinit { print("deinit:", value) }

  func debugValue() {
    print(value)
  }

  consuming func run1() {
    return
  }

  mutating func run2() {
    return
  }

  borrowing func run3() {
    return
  }

  // `borrowing`はデフォルトなので、`run4()`は`borrowing`として扱われる
  func run4() {
    return
  }
}

~Copyableな型のメソッドにconsumingを利用する

次の例を考えてみます。MyValueにはconsumingキーワードが付与されたrun1()メソッドがあります。このメソッドを呼び出すと、selfがメソッド内で消費されるため、メソッドの終了時にmyValueのライフサイクルも終了します。これにより、deinitが呼び出されます。

struct MyValue: ~Copyable {
  var value: Int = 0

  init() { print("init:", value) }
  deinit { print("deinit:", value) }

  func debugValue() {
    print(value)
  }

  consuming func run1() {
    self.value = 23
    // このメソッドを抜けるタイミングで`self`のライフタイムは終了し、`deinit`が呼ばれる
  }
}

func main() {
  let myValue = MyValue()
  myValue.run1()
}

discard selfについて

consumingキーワードを付与したメソッド内でselfのライフサイクルを終了させる処理を行いつつ、deinitを呼び出さないようにしたいケースも考えられます。なぜなら、deinit内に書かれた処理と、consumingキーワードを持つメソッドの処理が両方とも実行されてしまうからです。deinitの呼び出しを防ぐためには、メソッド内でdiscard selfを使用する必要があります。これにより、selfのライフサイクルは終了しますが、deinitは呼び出されません。

struct MyValue: ~Copyable {
  // 略
  consuming func run1() {
    self.value = 23
    discard self // これにより`deinit`は呼ばれなくなる
  }
}

しかし、discard selfを条件分岐内で使用するとき、その外でもselfの消費が必要となります。これを行わない場合はコンパイルエラーが発生します。

struct MyValue: ~Copyable {
  // 略
  consuming func run1() {
    guard someBoolValue else {
      discard self
      return
    }
  } // ERROR: Must consume 'self' before exiting method that discards self
}

これを防ぐためには分岐外でselfを消費させます。簡単に消費させる方法は_ = consume selfを利用することです。(consumeを書かずに_ = selfのように書いてもコンパイルエラー自体は回避できます。)
また、consumingキーワードがついたメソッドを呼び出してもコンパイルエラーを回避できます。

struct MyValue: ~Copyable {
  // 略
  consuming func run1() {
    guard someBoolValue else {
      discard self
      return
    }
    _ = consume self // ここで`self`が消費されるので、エラーは発生しない
  } 
}

注意点として、selfを2回以上消費するコードは、コンパイルエラーの原因となります。これはdiscard selfに関係なく発生します。

struct MyValue: ~Copyable { // ERROR: 'self' consumed more than once
  // 略
  consuming func run1() {
    if someBoolValue {
      discard self
    }
    _ = consume self 
  } 
}

discard selfの詳細な動作: メモリ領域の解放とパフォーマンス

discard selfを呼ぶことでdeinitは呼ばれなくなりますが、selfのライフサイクルは終了します。これを確かめてみます。

まずはサンプルコードとして以下を用意します。

サンプルコード

struct MyValue: ~Copyable {
  var value: Int = 0

  init() { print("init:", value) }
  deinit { print("deinit:", value) }

  func debugValue() {
    print(value)
  }

  borrowing func run0() {
    return
  }

  consuming func run1() {
    return
  }

  consuming func run2() {
    discard self
  }
  
  consuming func run3() {
    _ = consume self
  }
  
  consuming func run4() {
    _ = self
  }
}

このコードにはrun0(), run1(), run2(), run3(), run4()のメソッドが存在しています。

次に以下のコマンドswiftc -emit-sil main.swift -o main.silを実行し、上記のメソッドに対するSILを出力します。

run0(), run1(), run2(), run3(), run4()のメソッドそれぞれのSILは以下のようになります。

MyValue.run0()

sil hidden @$s4main7MyValueV4run0yyF : $@convention(method) (@guaranteed MyValue) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $MyValue):
  debug_value %0 : $MyValue, let, name "self", argno 1, implicit // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$s4main7MyValueV4run0yyF'

MyValue.run1()

sil hidden @$s4main7MyValueV4run1yyF : $@convention(method) (@owned MyValue) -> () {
// %0 "self"                                      // user: %2
bb0(%0 : $MyValue):
  %1 = alloc_stack [lexical] $MyValue, var, name "self", implicit // users: %2, %4, %3
  store %0 to %1 : $*MyValue                      // id: %2
  destroy_addr %1 : $*MyValue                     // id: %3
  dealloc_stack %1 : $*MyValue                    // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s4main7MyValueV4run1yyF'

run1()で注目すべき箇所は以下の二行です

destroy_addr %1 : $*MyValue                     // id: %3
dealloc_stack %1 : $*MyValue                    // id: %4

最初のdestroy_addr命令により、指定したアドレスに存在する値のデストラクタ(deinit)が呼び出されます。この操作によって、そのメモリ位置にある値が破棄されますが、メモリ領域自体はまだ解放されていません。

次のdealloc_stack命令は、メモリ領域を物理的に解放するための操作です。この操作により%1のメモリ領域をスタックから解放しています。

以上の解析から、consumingが付与されたメソッドでは、メソッドを抜けるタイミングでselfのライフタイムが終了し、かつ、deinitが呼ばれることがわかります。

MyValue.run2()

sil hidden @$s4main7MyValueV4run2yyF : $@convention(method) (@owned MyValue) -> () {
// %0 "self"                                      // user: %2
bb0(%0 : $MyValue):
  %1 = alloc_stack [lexical] $MyValue, var, name "self", implicit // users: %2, %3, %7
  store %0 to %1 : $*MyValue                      // id: %2
  %3 = begin_access [modify] [static] %1 : $*MyValue // users: %4, %6
  %4 = load %3 : $*MyValue
  debug_value undef : $*MyValue, var, name "self", implicit // id: %5
  end_access %3 : $*MyValue                       // id: %6
  dealloc_stack %1 : $*MyValue                    // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main7MyValueV4run2yyF'

run2()でdiscard selfを使用しているため、run1()と異なり、destroy_addr命令は呼ばれていません。これは、deinitが呼ばれないことを意味します。一方で、dealloc_stack命令は呼ばれており、メモリ領域は解放されています。これにより、deinitが呼ばれなくても、selfのライフタイムは終了し、適切にメモリが管理されていることが確認できます。

MyValue.run3()

sil hidden @$s4main7MyValueV4run3yyF : $@convention(method) (@owned MyValue) -> () {
// %0 "self"                                      // user: %2
bb0(%0 : $MyValue):
  %1 = alloc_stack [lexical] $MyValue, var, name "self", implicit // users: %2, %3, %11
  store %0 to %1 : $*MyValue                      // id: %2
  %3 = begin_access [modify] [static] %1 : $*MyValue // users: %5, %8
  %4 = alloc_stack $MyValue                       // users: %7, %5, %9, %10
  copy_addr [take] %3 to [init] %4 : $*MyValue    // id: %5
  debug_value undef : $*MyValue, var, name "self", implicit // id: %6
  %7 = load %4 : $*MyValue
  end_access %3 : $*MyValue                       // id: %8
  destroy_addr %4 : $*MyValue                     // id: %9
  dealloc_stack %4 : $*MyValue                    // id: %10
  dealloc_stack %1 : $*MyValue                    // id: %11
  %12 = tuple ()                                  // user: %13
  return %12 : $()                                // id: %13
} // end sil function '$s4main7MyValueV4run3yyF'

run3()の特徴な点は、selfを別のスタック領域にコピーしていることです。この操作のため、2つの異なるスタック領域(%1と%4)が確保され、それに応じて2回のdealloc_stackが必要になっています。

SILレベルで見ると、selfのコピー操作があるため、run3()のオーバーヘッドは最も大きいと考えられます。
(実際のパフォーマンスはコンパイラの最適化の程度によって異なる可能性があります。)

MyValue.run4()

sil hidden @$s4main7MyValueV4run4yyF : $@convention(method) (@owned MyValue) -> () {
// %0 "self"                                      // user: %2
bb0(%0 : $MyValue):
  %1 = alloc_stack [lexical] $MyValue, var, name "self", implicit // users: %2, %3, %8
  store %0 to %1 : $*MyValue                      // id: %2
  %3 = begin_access [modify] [static] %1 : $*MyValue // users: %4, %6
  %4 = load %3 : $*MyValue                        // user: %7
  debug_value undef : $*MyValue, var, name "self", implicit // id: %5
  end_access %3 : $*MyValue                       // id: %6
  release_value %4 : $MyValue                     // id: %7
  dealloc_stack %1 : $*MyValue                    // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function '$s4main7MyValueV4run4yyF'

run4()ではdestroy_addrが使用されていませんが、release_value %4 : $MyValueが明示的に使用されていることがポイントです。このrelease_valueが、selfの所有権を放棄し、それに伴ってdeinitが呼び出されるトリガーとなっていると考えれます。

ただし、selfをスタックからロードし、その後リリースする操作があるため、run2よりも若干のオーバーヘッドが追加されると予想されます。

まとめ

Swift 5.9の~Copyableの導入により意図しないコピーを防ぐことが可能になります。また、所有権の概念が強化され、メモリ管理がより強化されます。以下に、本記事で取り上げた主要なポイントを簡潔にまとめます。

  1. ~Copyable:
    • Swift 5.9では、~Copyableが導入されました。この新しい型はコピー不可能で、一意のリソースを効果的に表現することができます。
  2. 引数の指定:
    • メソッドの引数として~Copyableを使用する場合、borrowing, consuming, またはinoutのいずれのキーワードを指定する必要があります。
  3. borrowing & consuming:
    • borrowing: 一時的に所有権を借りるだけで、呼び出し元はその後も変数・定数を利用できます。
    • consuming: 呼び出し元の所有権は失われ、呼び出し先に所有権が移動します。
  4. ディスアセンブリの比較:
    • borrowingconsumingの挙動の違いは、ディスアセンブリを用いて確認できます。具体的には、consumingが指定されたメソッドの終了時にオブジェクトのライフサイクルが終了することが示されました。
  5. ~Copyableな型のメソッド:
    • borrowing, consuming, mutatingのキーワードの中から選択できますが、borrowingはデフォルト値として設定されています。
  6. discard self:
    • discard selfを使用することで、deinitを呼び出さずにselfのライフサイクルを終了させることができます。メモリ領域は解放されていますが、deinitが呼ばれないことが確認できました。

~Copyableの導入はSwiftにおいて不要なコピーを制限させ、メモリ管理を更に進化させます。この新しい機能によって、Swiftのコードの効率と安全性が向上することが期待されます。


開発環境

  • Swift compiler version info: Apple Swift version 5.9 (swiftlang-5.9.0.128.106 clang-1500.0.40.1)
  • Xcode version info: Xcode 15.0 Build version 15A5229m (beta 8)

Special Thanks

この記事を執筆する際に、多くの方々からのサポートを受けました。ありがとうございました。

  • iOSDC Japan 2023のアンカンファレンス「Swift 5.9の話 〜 所有権を10分でキャッチアップ」にて、内容をご覧になり、議論に参加してくださった皆様
  • Xでの意見交換・議論に参加してくださった皆様
  • 社内勉強会「SwiftWednesday」での議論に参加してくださった皆様

参考引用文献

Discussion