Swiftにおける全ての値は、値型または参照型に分類されます。
値型とはIntなどのSwift標準型やstructなど、変数への代入がなされた際に値がコピーされるもの、参照型とはclassなど代入がなされた際には値はコピーされず同じ参照先が代入されるものです。
値型は代入が起こった際に値のコピーが入るという性質上、生成された値に変更を加えない限りは常に同じ値を保つというメリットがあります。これをValue Semanticsといいます。
ここでは値型と参照型のメリット、Value Semanticsの実現方法について解説したいと思います。
値型 vs 参照型
値型とそのメリット
struct ValueType {
var intValue: Int
init(intValue: Int = 0) {
self.intValue = intValue
}
}
let c = ValueType(intValue: 0)
var d = c
d.intValue = 1
print(c.intValue) // 0
d
に変更を加えても、c
は何も変更を受けません。
d
にはc
がコピーされて代入されるためです。
値型は
- 複数生成してもそれぞれの指し示す値の意味は全く同じになる場合
- 生成後は独立している
ような値に適しています。一度アサインした値を変更するためには、そのものを置き換える必要があります。
数字や文字、データ、およびそれらを保持するだけの構造体等に使用されます。
SwiftUIの宣言的UIも、ここにこういうViewを配置する、という情報を持った構造体の集合と捉えれば、この値型の特徴をよく表していると言えます。
参照型とそのメリット
class ReferenceTypeClass {
var intValue: Int
init(intValue: Int = 0) {
self.intValue = intValue
}
func increment() {
intValue += 1
}
}
let a = ReferenceTypeClass()
let b = a
b.increment()
print(a.intValue) // 1
b
に変更を加えたにも関わらず、a
の値にまで変更が及んでしまっています。
これは、b
とa
に代入されている値が同じ参照先となっているためです。
参照型は
- 同種であっても複数生成時、一つ一つの値が区別できる
- 生成された後、他のものと相互に作用し合う
ような値に適しています。一度アサインした値を変更することは、内部変数への代入または直接書き換える関数等を呼び出すことで可能になっています。
UIApplication
などのUIKitフレームワーク等、生成された後に互いに相互作用し合うようなclass等に用いられます。
Value Semanticsの実装方法
値型しか持たないstruct
やenum
、tuple、Array
等はその生成時に自動で値がコピーされて入ります。そのため、ここでは以下の2パターンについて解説します。
- 参照型の値でValue semanticsを満たす場合
- 参照型の値を含む値型の値でValue semanticsを満たす場合
1. 参照型の値でValue semanticsを満たす場合
この場合のポイントは、内包している変数に変更可能な箇所がないということです。そのため、内包している変数全てがlet
で、かつ値型であればValue semantics条件を満たします。
class ReferenceTypeClass {
let intValue: Int
init(intValue: Int = 0) {
self.intValue = intValue
}
func increment() {
intValue += 1 // コンパイルエラー: Left side of mutating operator isn't mutable: 'intValue' is a 'let' constant
}
}
let a = ReferenceTypeClass()
let b = a
b.intValue = 1 // コンパイルエラー: Cannot assign to property: 'intValue' is a 'let' constant
print(a.intValue)
こうすることで、変数の外部から値を変えることができないことを保証でき、Value Semanticsの条件を満たしたと言えるでしょう。
2. 参照型の値を含む値型の値でValue semanticsを満たす場合
値型であるstruct
に参照型プロパティを持たせた場合、以下のようになります。
struct ValueType {
var intValue = 0
// 追加
var reference = ReferenceTypeClass(intValue: 0)
init(intValue: Int = 0) {
self.intValue = intValue
}
}
let c = ValueType(intValue: 0)
let d = c
d.reference.intValue = 1
print(c.reference.intValue) // 1
d
はc
のコピーではありますが、reference
自体は同じ参照先が登録されているので、d
で変更した値はc
にも反映されてしまいます。
これを防ぐためには、以下のように適切にaccess controlの設定、getterとsetterの設定をしてあげる必要があります。
struct ValueType {
var intValue = 0
// privateに変換
private var reference = ReferenceTypeClass(intValue: 0)
// getされる場合はprivate変数の値を、setされる場合は新しいインスタンスを生成する。
var referenceIntValue: Int {
get {
return reference.intValue
}
set {
reference = ReferenceTypeClass(intValue: newValue)
}
}
init(intValue: Int = 0) {
self.intValue = intValue
}
}
let c = ValueType(intValue: 0)
var d = c
d.referenceIntValue = 1
print(c.referenceIntValue) // 0
このようにすることで、reference
に何か値の変更が入った際に別の参照先に入れ替わるため、d
のreference
に変更を加えてもc
のreference
に影響が及ぶことはありません。このように、setterでのみ参照先を変更するこの仕組みをCopy of Writing(COW)といいます。
しかし、d
がない場合(つまり、同じ値を代入した変数が他にない場合)はset
のたびにインスタンスを入れ替える必要がありません。その場合は、内部のreference
の値を直接書き換えてやれば良いです。
その際、下記のようにする必要があります。
struct ValueType {
var intValue = 0
private var reference = ReferenceTypeClass(intValue: 0)
var referenceIntValue: Int {
get {
return reference.intValue
}
set {
// 追加
if isKnownUniquelyReferenced(&reference) {
reference.intValue = referenceIntValue
} else {
reference = ReferenceTypeClass(intValue: newValue)
}
}
}
init(intValue: Int = 0) {
self.intValue = intValue
}
}
let c = ValueType(intValue: 0)
var d = c
d.referenceIntValue = 1
print(c.referenceIntValue) // 0
結果は変わらずですが、インスタンス生成コスト分、set
処理が軽くなります。
余談: Sendableについて
SwiftではValue Semanticsを満たしていることを示すprotocolが存在します。それがSendable
です。公式ドキュメント
Sendable
に準拠するプロトコルは全て、Value Semanticsを満たしています(逆に全てのValue Semanticsを満たしているものがSendable
を満たすわけではありません)。
これはConcurrencyで複数の箇所で非同期処理を行っても、その値が変更されることがないため安全にデータを送信できるという意味でSendable
という名前がつけられています。
Sendable
である変数にValue Semanticsを満たしていない値を代入した際には、コンパイルエラーが発生します。
まとめ
- 生成された値に変更を加えない限りは常に同じ値を保つ機能をValue Semanticsという。
- 数字や文字、データ、およびそれらを保持するだけの構造体等、一度生成した値を書き換えるにはそのものを代入し直す必要があるものを値型といい、デフォルトでValue Semanticsを満たす。
- 逆に、classのように内部変数への代入または直接書き換える関数等を呼び出すことで値の書き換えが可能であるものを参照型といい、Value Semanticsを満たすためには以下条件が必要となる。
- 内包している変数全てが
let
で、かつValue Semanticsを満たしていること。
- 内包している変数全てが
- また、参照型を含むstructがValue Semanticsを満たすためには、access control, getterとsetterを適切に設定してあげる必要がある。
- Sendableであればvalue semanticsを満たす。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。