今更ながら『Heart of Swift』を読んだので、勉強用としてまとめる。
対外的にまとめるものではないです。
第1章 Value Semantics - この記事
第2章 Protocol-oriented Programming - 【勉強用】Heart of Swift - 2
はじめに
Swiftは値型を中心としたプログラミング言語である。
Heart of Swift では Swift の中心となる概念を通して Swift という言語のコンセプトが書かれている。
中心となる概念は以下2つ。
- Value Semantics
- Protocol-oriented Programming
これらについては WWDC2015 のセッションの中で詳しく説明されている。
Value Semantics とは
Value Semanticsの定義については Swift リポジトリの中のドキュメントの“Value Semantics in Swift”に書かれている。
"Value Semantics in Swift"では、SwiftにおけるValue Semanticsを次のように定義している。
For a type with value semantics, variable initialization, assignment, and argument-passing each create an independently modifiable copy of the source value that is interchangeable with the source.
(和訳)ある型が Value Semantics を持っているとき、その型の変数を初期化したり、値を代入したり、引数に渡したりすると、元の値のコピーが作られて、そのコピーと元の値は独立に変更することができる。つまり、どちらかを変更してももう片方には影響を与えない。
Value Semanticsを持つ例
struct Foo {
var value: Int = 0
}
var a = Foo()
var b = a
a.value = 2
print(a.value) // 2
print(b.value) // 0
a
と b
は全く同じ内容のバイト列が書かれているが、異なる領域に書かれた2つのデータであり、別々の Foo
インスタンスである。
そのため a.value
を変更しても b.value
は変更されない。
この場合、a
と b
は変更に対して独立であるため Foo
はValue Semanticsを持っていると言える。
Value Semanticsを持たない例
class Foo {
var value: Int = 0
}
var a = Foo()
var b = a
a.value = 2
print(a.value) // 2
print(b.value) // 2
参照型のインスタンスは変数に直接格納されず、インスタンスが格納されている別領域のメモリのアドレスが変数に格納される。(例: 0x123ABC
)
a
と b
には同じ 0x123ABC
というアドレスが格納されているため同一の Foo
インスタンスを参照することになる。
そのため、 a.value
を変更すると b.value
も変更されてしまう。
このような片一方を変更するともう一方も変更されてしまう場合、Value Semanticsと対比して、Reference Semanticsを持っていると言われる。
Semantics vs Type
Value Semanticsは値型と(Value Type)と、Reference Semanticsは参照型(Reference Type)と深い関係があるが同じものではない。
- Value Semantics ≠ 値型
- Reference Semantics ≠ 参照型
例えば、値型だがValue Semanticsを持たない型や、参照型だがValue Semanticsを持つ型も存在する。
値型だがValue Semanticsを持たない例
class Bar {
var value: Int = 0
}
struct Foo {
var value: Int = 0
vat bar: Bar = Bar()
}
var a = Foo()
var b = a
a.value = 2
a.bar.value = 3
print(a.value) // 2
print(a.bar.value) // 3
print(b.value) // 0
print(b.bar.value) // 3
このとき、Foo
は値型なのでa
、b
にはそれぞれ独立したFoo
インスタンスが格納されているため、a.value
を変更してもb.value
には影響を与えない
しかしBar
は参照型であるため、a
とb
のbar
プロパティには同じBar
インスタンスのアドレスが格納され同じインスタンスを参照しており、a.bar.value
に変更を加えるとb.bar.value
も変更される。
そのためFoo
は値型であるにも関わらず変更に対する独立性を持たない、
Value Semanticsを持たないことになる。さらに、a.value
とb.value
は独立して変更できるのでReference Semanticsも持たないことになる。
こういった型を作らないよう注意が必要だが、以下のようなクラスのインスタンスを値型のプロパティに持たせるとFoo
のようなValue SemanticsもReference Semanticsも持たない型ができてしまう。
- NSMutableArray、NSMutableString、NSMutableData
- UILabel、UISwitch、UISlider
- AVAudioPlayer
- CMMotionManager
しかし、「ミュータブルな参照型のプロパティを持つ場合はValue Semanticsを持たない」というパターンで判断するのは危険。
標準ライブラリのArray
は内部にミュータブルな参照型を保持しているが、Copy-on-Writeという仕組みを使ってValue Semanticsを実現している。
参照型プロパティを持つ値型でもValue Semanticsを持つ例
final class Bar {
let value: Int = 0
}
struct Foo {
var value: Int = 0
vat bar: Bar = Bar()
}
var a = Foo()
var b = a
a.value = 2
a.bar.value = 3 // ここでコンパイルエラー
Bar
をfinal class
に、value
プロパティをlet
にして、イミュータブルクラスへ変更する。(final class
にするのは、Bar
のミュータブルなサブクラスが作られてしまうとBar
型のイミュータビリティが破壊されてしまうため)
この時a.bar.value
を変更しようとするとコンパイルエラーになる。
a.bar
とb.bar
は同じインスタンスを参照しているが、Bar
はイミュータブルであるためそのインスタンスを通じて状態を変更することはできない。
そのためBar
型のプロパティを持つことがFoo
インスタンスの変更に対する独立性を破壊することにはつながらず、結果としてFoo
はValue Semanticsを持つということになる。
このようなケースはよく見られ、以下のようなクラスのインスタンスをプロパティに保持してもValue Semanticsを破壊する原因にはならない。
- NSNumber、NSNull
- UIImage
- KeyPath
”Value Semantics in Swift"には、イミュータビリティを持つ場合はValue SemanticsとReference Semanticsが区別できない、と書かれている。
そのため、
final class Foo {
let value: Int = 0
}
のようなイミュータブルなFoo
クラスはValue SemanticsとReference Semanticsの両方を持つと言える。
また、イミュータブルであれば値型であってもReference Semanticsを持つと言える。
struct Foo {
let value: Int = 0
}
Value Semanticsを持たない型の問題と対処法
Value Semanticsを持たない型が起こしがちな問題
Value Semanticsを持たない型が起こしがちな問題の例として以下2つが挙げられる。
- 意図しない変更
- 整合性の破壊
意図しない変更の例
class Item {
var name: String
var settings: Settings
init(name: String, settings: Settings) {
self.name = name
self.settings = settings
}
}
class Settings {
var isPublic: Bool
init(isPublic: Bool) {
self.isPublic = isPublic
}
}
let item: Item = .init(name: "hoge", settings: .init(isPublic: false))
let duplicated: Item = .init(name: item.name, settings: item.settings)
duplicated.settings.isPublic = true
print(item.name) // hoge
print(item.settings.isPublic) // true
print(duplicated.name) // hoge
print(duplicated.settings.isPublic) // true
Settings
クラスはReference Semanticsを持っているため、duplicated
変数のisPublic
の変更がオリジナルのitem.settings
にも波及してしまっている。
Item
インスタンスを複製する際にSettings
インスタンスも複製しなければならなかった。ディープコピーしなければならなかったのに、シャローコピーしてしまったことが原因。
let item: Item = .init(name: "hoge", settings: .init(isPublic: false))
let duplicated: Item = .init(name: item.name, settings: .init(isPublic: item.settings.isPublic))
duplicated.settings.isPublic = true
整合性の破壊の例
class Person {
var firstName: String {
didSet { _fullName = nil }
}
var familyName: String {
didSet { _fullName = nil }
}
private var _fullName: String? // キャッシュ
var fullName: String {
if let fullName = _fullName {
return fullName
}
_fullName = "\(firstName)\(familyName)"
return _fullName!
}
init(firstName: String, familyName: String, _fullName: String? = nil) {
self.firstName = firstName
self.familyName = familyName
self._fullName = _fullName
}
}
let person: Person = .init(
firstName: "Taylor",
familyName: "Swift"
)
var familyName: String = person.familyName
familyName.append("y")
print(person.familyName) // "Swifty"
print(person.fullName) // "Taylor Swift"
こういった処理がある時、仮にString
がReference Semanticsを持ったクラスだった場合を考えてみる。
その時、familyName
の値もperson.familyName
の値も"Swifty"
に書き換えられるのだが、fullName
のキャッシュに"Taylor Swift"
という文字列が保存されている場合、状態更新時にはキャッシュをクリアするよう設計したのにfullName
が出力する値は"Taylor Swift"
のままになってしまう。
Person
インスタンスの状態の整合性が破壊されてしまっている。
しかしperson.familyName
を直接"Swifty"
に更新する場合は、キャッシュが適切にクリアされて問題は起きない。
print(person.fullName) // "Taylor Swift"
person.familyName = "Swifty"
print(person.familyName) // "Swifty"
print(person.fullName) // "Taylor Swifty"
firstName
プロパティやfamilyName
プロパティなど、Person
のAPIを使わずにString
のメソッドを介して間接的に内部状態が変更されるという想定しない変更のパスがあったことが原因。
Reference Semanticsはこういったパスを作りやすいため、設計時に見落としてしまうと例のように内部的な整合性の破壊が起きてしまうことがある。
問題への対処法
こういった問題は参照型中心の言語ではよく起こるため様々な対処法が考えられている。
よく取られる対処法としては以下の3つ
- 防御コピー
- Read-only View
- イミュータブルクラス
しかし、Swiftは参照型中心ではなく値型中心の言語であるため、値型を用いたアプローチで問題に対処する。
値型を用いた問題の解決方法
「意図しない変更」の例では、Settings
がValue Semanticsを持ったstruct
であればそもそも問題が起きない。
let duplicated: Item = item // 複製
duplicated.settings.isPublic = true // item.settings.isPublicは変更されない
「整合性の破壊」の例でも、変数falimyName
に対する変更はperson
に影響を及ぼさないため整合性の破壊は起きない。
print(person.fullName) // "Taylor Swift"
var familyName: String = person.familyName
familyName.append("y") // personには影響を及ぼさない
print(person.familyName) // "Swift"
print(person.fullName) // "Taylor Swift"
さらに値型にはミュータブルクラスのように変更が容易であるという特徴がある。
つまり、値型はミュータブルクラスの持つ変更の容易さと、イミュータブルクラスの持つValue Semanticsのいいとこ取りをしたような存在だと考えられる。
Swiftの標準ライブラリで提供されている型はほぼ全てが値型。
- Int、Float、Double、Bool
- String、Character
- Array、Dictionary、Set
- Optional、Result
Swiftが値型中心の言語になれた理由とその使い方
Swiftの標準ライブラリはコレクションも値型
SwiftのコレクションはCopy-on-Writeという仕組みを使ってValue Semanticsとパフォーマンスを両立している。
ref: "Copy-on-Write"
var a = [1,2,3]
var b = a
a[2] = 4
print(b) // [1,2,3]
Swiftは値型の使い勝手を向上させる言語仕様が豊富
inout
引数
Int
型のpoints
変数を持つ、値型のUser
がある時、ポイントを消費する処理は以下のように書ける。
func consumePoints(_ points: Int, of user: inout User) throws {
guard let points <= user.points else {
throw PointsShortError()
}
user.points -= points
}
var user: User = .init(...)
try consumePoints(100, of: &user)
inout
引数は、引数に渡された値が関数の中で変更された場合、呼び出し元へ反映するというもの。
一歩間違えるとValue Semanticsを破壊する危険なものであるため、inout
や&
を記述して値型にとって不自然な挙動を引き起こすための明示的な意思表示が必要であったり、inout
な変数やプロパティを作ったりinout
な戻り値を作ったりすることはできないように設計されている。
mutating func
関数ではなくUser
のメソッドとして実装する場合は以下のようになる。
extension User {
mutating func consumePoints(_ points: Int) throws {
guard points <= self.points else {
throw PointsShortError()
}
self.points -= points
}
}
var user: User = .init(...)
try user.consumePoints(100)
mutating
を付与することは暗黙の引数self
にinout
を付けるのと同じ意味。
Conputed Propertyを介した変更
SwiftではComputed PropertyをStored Propertyと同じように扱うことができる。Computed Propertyを介して状態を変更することも可能。
struct Group {
var owner: User {
get { members[0] }
set { members[0] = newValue }
}
...
}
group.owner.points = 0 // ✅
Computed Propertyはメソッドのようなものであるため、owner
がComputed Propertyではなくメソッドだと考える。
この時User
が値型の場合、owner
メソッドの戻り値に対して変更を加えようとするとコンパイルエラーになる。
これはgroup.owner()
の戻り値はreturnされる際にコピーされるものであり、コピーされたUser
インスタンスは一時的にメモリ上に存在するだけであるため変更を加えてもその結果は破棄されてしまう。
こういった無意味なケースをSwiftコンパイラはコンパイルエラーとして検出する。
しかしowner
がComputed Propertyの場合は以下のような手順で処理が行われる。
-
group.owner
のget
を用いてUser
インスタンスのコピーが返される - そのコピーの
points
が0に変更される - 変更を加えられたコピーが
group.owner
のset
のnewValue
として渡され、変更がgroup
に反映される
また、Computed Propertyに加えてsubscript
を介した変更も同じように動作する。
group.members[i].points = 0 // ✅
これも
-
subscript
のget
-
points
の変更 -
subscript
のset
という順で実行される。
また、Computed Propertyやsubscript
を介した変更は、inout
引数やmutating func
と組み合わせることもできる。
// inout 引数との組み合わせ
try consumePoints(100, of: &group.owner) // ✅
// mutating func との組み合わせ
try group.owner.consumePoints(100) // ✅
高階関数とinout
引数の組み合わせ
複数のUser
、[User]
について考える。
もしUser
がミュータブルクラスであるなら単純にfor-in
ループで値を1つずつ取り出して変更を加えるだけだが、値型の場合はfor-in
ループのループ変数はデフォルトで定数であるためコンパイルエラーになってしまう。
また、ループ変数を変数にした場合コンパイルは通るようになるが、1つのデータをコピーしてループ変数に格納しているため、コピーへの変更がオリジナルに反映されない。
こういったケースの対処法としては、
- インデックスを介して状態を更新する
-
inout
引数を持つmutating func
を用いる
という方法がある。
var users: [User] = ...
// for-inループ
for user in users {
user.points += 100 // コンパイルエラー
}
// for-inループのループ変数を変数に
for var user in users {
user.points += 100 // コンパイルは通るが、実行しても何も起きない
}
// インデックスを介して状態を変更する
for i in users.indices {
users[i].points += 100 // コンパイルが通り、変更も反映される
}
// inout引数とmutating funcを用いる
extension MutableCollection {
mutating func modifyEach(_ body: (inout Element) -> Void) {
for i in indices {
body(&self[i])
}
}
}
users.modifyEach { user in
user.points += 100 // コンパイルが通り、変更も反映される
}
また、このmodifyEach
相当のことは将来的にはfor-in
ループとinout
の組み合わせでできるようになる可能性がある。
// 将来的には以下の形になるかもしれない
for inout user in users {
user.points += 100
}
値型とミュータブルクラスの使い分け
Swiftにおいてイミュータブルクラスを作るべきケースはほとんど存在しない。
まず(Value Semanticsを持つ普通の)値型で実装し、それでうまくいかない場合にミュータブルクラス(Reference Semantics)を検討するという方法がおすすめ。
そうすることでValue Semanticsを持たない型の利用を最小限に留められ、「意図しない変更」や「整合性の破壊」などの発生機会を最小化し問題のコントロールが容易になる。
Value Semanticsを持つ世界・持たない世界を分離しておくことが重要。