Swift 5.9の新機能:`~Copyable`と所有権について
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
この例では、resource1
とresource2
は同じクラスのインスタンスを参照していますが、そのことがヒープ上でのオブジェクトのライフサイクルの管理や、リソースのアクセスを調整するための追加のオーバーヘッドを発生させる問題点となります。
コピー不可能な構造体や列挙型の概念が導入
Swift5.8以前のバージョンでは構造体や列挙型はコピー可能でしたが、SE-0390では、コピー不可能な構造体や列挙型の概念が導入されます。
これにより、コピー不可能な型の値は、常に一意な所有権を持ち、その値はコピーすることができなくなります。
さらに、この型の値は一意のアイデンティティを持つので、struct
に対してdeinit
の実装が可能となります。
Copyableの制約とは?
Swiftにおいて型のコピーは、これまでのバージョンではほとんどが暗黙のうちに行われていました。このことは、ある意味でSwiftの多くの型がどれもコピー可能であったことを示しています。
しかし、Swift 5.9では、型がコピーが可能であることを明示するための新しい制約Copyable
が導入されます。
Swiftの標準ライブラリに、Copyable
制約が含まれるようになり、これにより、すでに存在するほとんどの主要な型はこの制約を暗黙的に満たすようになり、Generics
やProtocol
などにもこの制約を暗黙的に必要としています。
しかし、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
}
この~
は否定を意味します。したがって、~Copyable
はCopyable
ではない、つまりコピー不可能であることを示しています。
~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()
}
~Copyable
なstruct
は一意のアイデンティティを持つため、これにより、struct
でもclass
と同様にdeinit
を使用することができます。
struct MyValue: ~Copyable {
var value: Int = 0
deinit { // deinit を使用することができる
print("deinit:", value)
}
}
~Copyable
の挙動を深掘り
所有権の移動: 前のセクションでは、~Copyable
を利用したコピー不可能な型の宣言方法を紹介しました。では、~Copyable
で宣言された型は具体的にどのような挙動を示すのでしょうか?特に、~Copyable
という制約によって強調されている「所有権(ownership
)」という概念は、Swiftのプログラムの安全性と効率性を向上させるための鍵となる要素です。
所有権の移動とは、一度変数や定数に値が代入された後、その変数や定数を他の変数や定数に代入することで、最初の変数や定数の使用権限が無効となることを指します。以下の例を参照してください。
まず、MyValue
のインスタンスをmyValue
に代入します。この時点でmyValue
はそのインスタンスの所有権を持っています。しかし、myValue
をyourValue
に代入すると、myValue
の所有権は失われ(myValue
がconsume
、つまり消費されます)、その後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レベルで変数のライフサイクルがどのように変化するかを見る
SIL
(Swift 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
のメモリ領域にコピーされます。このプロセスは、以下のload
とstore
の命令によって示されています。
%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)
}
では、実際にborrowing
とconsuming
を指定した場合にどのような変化が発生するかを見ていきます。
borrowing
を指定する場合
borrowing
を指定する場合、引数のmyValue
はimmutable
となります。そのため、メソッドで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
が指定された場合、メソッドを抜けるタイミングでオブジェクトのライフサイクルが終了することを紹介しました。しかし、この挙動は具体的にどのように実現されているのでしょうか? borrowing
とconsuming
の挙動の違いを、ディスアセンブリを用いて確認してみましょう。
サンプルコード
func doSomething1(myValue: borrowing MyValue) {
return
}
func doSomething2(myValue: consuming MyValue) {
return
}
以下の手順で、doSomething1
とdoSomething2
のディスアセンブリを実行します:
swiftc main.swift -o main
lldb main
disassemble --name doSomething1
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
これらのディスアセンブリ結果から、以下の点に着目することができます
-
consuming
を使用したdoSomething2
では、メソッドを抜ける直前にmain.MyValue.deinit
が呼び出されています。これは、myValue
のライフサイクルがメソッドの終了時点で終了していることを示しています。 - 一方、
borrowing
を使用したdoSomething1
では、deinit
は呼び出されていません。これは、オブジェクトのライフサイクルが継続していることを示しています。
この比較から、consuming
とborrowing
を利用した場合のライフサイクルの違いを確認することができます。
~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
の導入により意図しないコピーを防ぐことが可能になります。また、所有権の概念が強化され、メモリ管理がより強化されます。以下に、本記事で取り上げた主要なポイントを簡潔にまとめます。
-
~Copyable
:- Swift 5.9では、
~Copyable
が導入されました。この新しい型はコピー不可能で、一意のリソースを効果的に表現することができます。
- Swift 5.9では、
- 引数の指定:
- メソッドの引数として
~Copyable
を使用する場合、borrowing
,consuming
, またはinout
のいずれのキーワードを指定する必要があります。
- メソッドの引数として
-
borrowing
&consuming
:-
borrowing
: 一時的に所有権を借りるだけで、呼び出し元はその後も変数・定数を利用できます。 -
consuming
: 呼び出し元の所有権は失われ、呼び出し先に所有権が移動します。
-
- ディスアセンブリの比較:
-
borrowing
とconsuming
の挙動の違いは、ディスアセンブリを用いて確認できます。具体的には、consuming
が指定されたメソッドの終了時にオブジェクトのライフサイクルが終了することが示されました。
-
-
~Copyable
な型のメソッド:-
borrowing
,consuming
,mutating
のキーワードの中から選択できますが、borrowing
はデフォルト値として設定されています。
-
-
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