0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【勉強用】Heart of Swift - 1

Last updated at Posted at 2024-09-15

今更ながら『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

ab は全く同じ内容のバイト列が書かれているが、異なる領域に書かれた2つのデータであり、別々の Foo インスタンスである。
そのため a.value を変更しても b.value は変更されない。

この場合、ab は変更に対して独立であるため FooValue 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)
ab には同じ 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は値型なのでabにはそれぞれ独立したFooインスタンスが格納されているため、a.valueを変更してもb.valueには影響を与えない

しかしBarは参照型であるため、abbarプロパティには同じBarインスタンスのアドレスが格納され同じインスタンスを参照しており、a.bar.valueに変更を加えるとb.bar.valueも変更される。

そのためFooは値型であるにも関わらず変更に対する独立性を持たない、
Value Semanticsを持たないことになる。さらに、a.valueb.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 // ここでコンパイルエラー

Barfinal classに、valueプロパティをletにして、イミュータブルクラスへ変更する。(final classにするのは、Barのミュータブルなサブクラスが作られてしまうとBar型のイミュータビリティが破壊されてしまうため)
この時a.bar.valueを変更しようとするとコンパイルエラーになる。

a.barb.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を付与することは暗黙の引数selfinoutを付けるのと同じ意味。

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の場合は以下のような手順で処理が行われる。

  1. group.ownergetを用いてUserインスタンスのコピーが返される
  2. そのコピーのpointsが0に変更される
  3. 変更を加えられたコピーがgroup.ownersetnewValueとして渡され、変更がgroupに反映される

また、Computed Propertyに加えてsubscriptを介した変更も同じように動作する。

group.members[i].points = 0 // ✅

これも

  1. subscriptget
  2. pointsの変更
  3. subscriptset

という順で実行される。
また、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を持つ世界・持たない世界を分離しておくことが重要。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?