第 1 章  Value Semantics

Swift が値型中心の言語になれた理由とその使い方

前ページで見たように、 Swift の標準ライブラリで提供される型はほぼすべてが 値型Value Semantics を持っています。このような言語は稀です。 値型ミュータブルクラスイミュータブルクラス のいいとこ取りをできる優れものなら、なぜ他の言語はそれを採用しないのでしょうか。 Swift にそれができた理由は何でしょうか。筆者は、次の二つがその理由だと考えています。

これらに基づいて Swift が 値型 中心の言語になれた理由を示し、また、 値型 に適したコードの書き方を説明します。

Swift の標準ライブラリはコレクションも値型

多くの言語では、 ArrayDictionary などのコレクションは 参照型 です。

たとえば、 PythonList ( Swift の Array に相当)は 参照型 なので、次のようなことが起こります。

# Python
a = [2, 3, 4]
b = a
a[2] = 5
print(b)  # [2, 3, 5]

Reference Semantics を持っているので a[2] を変更すると b[2] も変更されました。

同じことを Swift の Array ですると次のようになります。

// Swift
var a = [2, 3, 4]
var b = a
a[2] = 5
print(b) // [2, 3, 4]

Value Semantics を持っているので a[2] を変更しても b[2] には影響を与えません。このように、 Swift のコレクションは他の多くの言語のコレクションと異なります。

しかし、コレクションが 値型 なのは一見すると合理的でないように思えます。 値型 のインスタンスは変数に代入されたり引数に渡されたりする度にコピーされます。 100 万個の要素を持つ Array インスタンスが関数の引数に渡されただけで 100 万個丸ごとコピーされるようでは、パフォーマンスが悪すぎて使い物になりません。

let a = Array(1...1_000_000)
sum(of: a) // コピーされる?🤔

Swift のコレクションは、 Copy-on-Write という仕組みを使って Value Semantics とパフォーマンスを両立しています。 Copy-on-Write は、簡単に言うと必要なときだけコピーが行われる仕組みです。上記の例では、 sum(of:) に渡された Array インスタンスが sum(of:) の中で変更されることはないので、 Array が共有されても変更の独立性に影響を与えません。そのためコピーは必要なく、省略されます。 Copy-on-Write の挙動と仕組みについての詳細は “付録” > “Copy-on-Write” を御覧下さい。

コレクションを使わずにコードが書けることは滅多にありません。コレクションが 参照型 であるということは( イミュータブル なコレクションしか使わないというのでない限り)、「意図しない変更」や「整合性の破壊」のような問題と付き合っていかないといけないということです。

たとえば、 C# は Swift と似た struct を持ちますが、コレクションは 参照型 です。そのため、 値型 中心のコードを書こうとしてもすぐに 参照型 が混入しがちです。 Swift は Copy-on-Write を用いることで 値型 のコレクションを実現しました。コレクションも 値型 なので、 Swift では 値型Value Semantics 中心のコードを書くことが可能なわけです。

Swift は値型の使い勝手を向上させる言語仕様が豊富

値型 中心の言語を実現するために必要なのは、 値型 中心の標準ライブラリを提供することだけではありません。 値型 中心の標準ライブラリがあっても、肝心の 値型 の使い勝手が悪いと単に不便なだけです。

前ページミュータブルクラス値型 のコードを比較し、 値型ミュータブルクラス のように変更が容易であると説明しました。しかし、前ページのような単純なケースではなく、より複雑なケースでは一筋縄にいかないこともあります。 Swift にはそのようなケースに対処するための言語仕様が豊富で、 値型 の使い勝手向上につながっています。

ここでは、次の各項について Swift が 値型 の使い勝手をどのように改善しているかを見ていきます。

安全な inout 引数

前ページでは User にポイントを付与する例を取り上げましたが、逆にポイントを消費する例を考えてみましょう。単純に考えれば次のようになります。

user.points -= 100

しかし、これではポイントが 0 より小さくなってしまうかもしれません。ポイントを消費するコードを書く度にポイントが足りているかチェックするのは大変なので、チェックを含めて consumePoints という関数にしましょう。

Userミュータブルクラス の場合には次のように書けます。

// ミュータブルクラスの場合
func consumePoints(_ points: Int, of user: User) throws {
    guard user.points >= points else {
        throw PointsConsumptionError()
    }
    user.points -= points
}

let user: User = ...
try consumePoints(100, of: user)

guard 文でポイントが足りるかをチェックし、足りない場合はエラーを throw します。ポイントが足りるなら user.points -= points でポイントを減らします。

値型 で同じことをしようとするとどうなるでしょうか。 Userstruct の場合、同様のコードはコンパイルエラーになります。引数で渡された user は定数扱いなので変更することができません。

// 値型の場合
func consumePoints(_ points: Int, of user: User) throws {
    guard user.points >= points else {
        throw PointsConsumptionError()
    }
    user.points -= points // ⛔
}

では、次のように一度変数に代入してから変更すれば良いのでしょうか。しかし、この場合も UserValue Semantics を持っているため、関数の呼び出し元に変更を反映することはできません。

// 値型の場合
func consumePoints(_ points: Int, of user: User) throws {
    var user: User = user // 変更できるように変数に代入してみたが
    guard user.points >= points else {
        throw PointsConsumptionError()
    }
    user.points -= points // 呼び出し元に変更が反映されない😵
}

var user: User = ...
try consumePoints(100, of: user) // ポイントは減らない😵

この問題を解決してくれるのが inout 引数です。 inout 引数を使えば、 値型 のための consumePoints を次のように書けます。

// 値型の場合
func consumePoints(_ points: Int, of user: inout User) throws {
    guard points <= user.points else {
        throw PointsShortError()
    }
    user.points -= points // コンパイルが通り、変更も反映される✅
}

var user: User = ...
try consumePoints(100, of: &user) // ポイントが減る😄

inout 引数は、引数に渡された値が関数の中で変更された場合、その変更を呼び出し元に反映します。そのため、 consumePoints の中で user のポイントを減らすと、それを呼び出し元の User インスタンスに反映できます。

ミュータブルクラス の場合のコードと比べても、関数の宣言部で inout が、呼び出し元のコードで & が増えただけで、コードを書く労力はほとんど変わりません。 inout& を記述しなければならないのも、手間が増えたというよりも 値型 にとって不自然な挙動を引き起こすための明示的な意思表示と考えられ、有用です。

値型 にとって不自然な挙動」と書きましたが、 inout 引数のような機能は、一歩間違えると Value Semantics を破壊する危険なものです。 Swift は、 inout 引数を通して危険な操作ができないように注意深く設計されています。 Swift の inout は第一級ではなく、 inout な変数やプロパティを作ったり、 inout な戻り値の型を作ったりすることはできません。

// Swift ではこんなことはできない⛔
func foo(_ x: inout Int) -> inout Int {
    return &x
}

var a = 2
var b = &foo(&a)
a = 3    // b も変更される
print(b) // 3

PHP ではこれができるため、 Value Semantics を破壊することができます。

<?php
function &foo(&$x) {
  return $x;
}

$a = 2;
$b = &foo($a);
$a = 3;  // b も変更される
echo $b; // 3
?>

このように、 inout のような機能は便利ですが、一方で Value Semantics を破壊する可能性を秘めた諸刃の剣でもあります。 Swift の inout 引数は Value Semantics を破壊しないように注意深く設計されているため、安心して利用することができます。

mutating func

consumePoints を関数ではなくメソッドとして実装してみましょう。

ミュータブルクラス の場合、 consumePoints メソッドは次のように書けます。

// ミュータブルクラスの場合
extension User {
    func consumePoints(_ points: Int) throws {
        guard self.points >= points else {
            throw PointsConsumptionError()
        }
        self.points -= points
    }
}

let user: User = ...
try user.consumePoints(100)

値型 では、メソッドがインスタンスに変更を加える場合に mutating 修飾子が必要になります。 consumePoints メソッドも points プロパティに変更を加えるため、 mutating func でなければなりません。

// 値型の場合
extension User {
    mutating func consumePoints(_ points: Int) throws {
        guard points <= self.points else {
            throw PointsShortError()
        }
        self.points -= points
    }
}

var user: User = ...
try user.consumePoints(100)

この mutating 修飾子は何のためにあるのでしょうか。 consumePoints 関数と consumePoints メソッドを比較して考えてみましょう。

そもそも、メソッドは暗黙的な引数 self を省略した関数とみなすことができます。

// 関数
func bar(_ self: Foo, _ x: Int) {
    print(self.value + x)
}

// メソッド
extension Foo {
    // 暗黙の引数 `self` を持つ
    func bar(_ x: Int) {
        print(self.value + x)
    }
}

上記の bar 関数と bar メソッドについて、次のように呼び出すと同じ結果が得られます。

let foo: Foo = ...

// 関数
bar(foo, 42)

// メソッド
foo.bar(42)

メソッドにおける暗黙の引数 self という考え方は一般的なもので、 Python 等の一部の言語ではメソッドの宣言時に self を明記することが求められます。

# Python
class Foo:
    def bar(self, x):  # 暗黙の引数 `self` を明記
        print(self.value + x)
    ...

foo.bar(42)  # 利用時には引数として渡す必要はない

関数とメソッドのそのような関係を意識して二つの consumePoints を見比べてみます。

// 値型の場合

// 関数
func consumePoints(_ points: Int, of user: inout User) throws {
    guard points <= user.points else {
        throw PointsShortError()
    }
    user.points -= points
}
try consumePoints(100, of: &user)

// メソッド
extension User {
    mutating func consumePoints(_ points: Int) throws {
        guard points <= self.points else {
            throw PointsShortError()
        }
        self.points -= points
    }
}

try user.consumePoints(100)

そうすると、 consumePoints 関数の inout 引数 user が、 consumePoints メソッドの暗黙の引数 self に相当していることがわかります。 値型 の値に変更を加えてそれを呼び出し元に反映するには引数に inout を付与する必要がありました。同様に、暗黙の引数 self に変更を加えて呼び出し元のインスタンスに変更を反映するにはメソッドに mutating 修飾子を付与する必要があるわけです。 mutating を付与することは、暗黙の引数 selfinout を付けるのと同じ意味を持ちます。

inout 引数に定数を渡せないのと同じように、定数に対して mutating func を呼び出すことはできません(渡せないのは定数だけでなく、 左辺値 でない値を渡すことはできません)。たとえば、下記のコードでは owner プロパティに対して consumePoints を呼び出して状態を変更しようとしていますが、 owner プロパティは let で宣言されているので、関数とメソッドのどちらの場合もコンパイルエラーになります。

struct Group {
    let owner: User
    ...
}

var group: Group = ...
try consumePoints(100, of: &group.owner) // ⛔
try group.owner.consumePoints(100)       // ⛔

この挙動は当たり前に思えるかもしれませんが、 C# の struct は異なる挙動をします。同様のコードを C# で書くと、 group.owner.ConsumePoints(100) はコンパイルエラーにならず、実行してもポイントは減りません。

// C#
public struct Group {
    public readonly User owner;
    ...
}

ConsumePoints(100, ref group.owner); // ⛔
group.owner.ConsumePoints(100); // コンパイルが通り、実行してもポイントは減らない😵

どうしてこんなことが起こるのでしょうか。

C# は inout 引数相当の言語仕様を持ちますが、 mutating func に相当するものを持ちません。そのため、 nonmutating なメソッドと mutating なメソッドをコンパイラが区別して扱うことができません。これは 値型 定数( C# では readonly )のメソッドを呼び出した際の挙動について悩ましい問題を生みます。

nonmutating なメソッドと mutating なメソッドを区別できないということは、メソッドを呼び出すとインスタンスに変更が加えられる可能性を常に考慮しなければならないということです。 値型 定数に対してメソッドを呼び出し、メソッドがインスタンスを変更しようとすると何が起こるでしょうか。そのような事態を避けるには、コンパイラが 値型 定数に対する一切のメソッド呼び出しを禁止してエラーにしてしまうという方法があります。しかし、さすがにそれは使い勝手の面で現実的ではありません。

そこで、 C# では 値型 定数のメソッドを呼び出す場合、インスタンスの暗黙的なコピーを生成し、そのコピーのメソッドを呼び出すという戦略をとっています。 Swift のコードで表すと、 Foo値型fooFoo 型の定数だとして、 foo.bar() というメソッド呼び出しは次のコードのような挙動をします。

var _foo: Foo = foo // 暗黙的なコピーを作成
_foo.bar() // 暗黙的なコピーのメソッドを呼び出す

暗黙的なコピー _foo に対して加えられた変更は即座に破棄されるため、 foo が変更されてしまうことはありません。これによって、 値型 定数のメソッド呼び出しと、 値型 定数が変更されないことを両立しているのです。

しかし、 C# のこの仕様は、前述のようにインスタンスの状態変更が無視されるという結果を生みます。 値型 定数の状態の変更を意図したコードはミスである可能性があり、ミスはコンパイラによって検出されるのが望ましいです。そのようなミスがコンパイルエラーどころか実行時エラーにもならずに黙殺されると、原因究明の難しいバグにつながりかねません。 Swift は inout 引数同様に mutating func を通常のメソッドと区別して扱うことで、このような問題を防止しています。

Computed Property を介した変更

Swift では、 Computed Property を Stored Property と同じように扱うことができます。 Computed Property を介して状態を変更することも可能です。

先程の owner プロパティが Computed Property だったとしましょう。

struct Group {
    var owner: User {
        get { members[0] }
        set { members[0] = newValue }
    }
    ...
}

この場合でも、 owner プロパティを介して状態を変更することができます。

group.owner.points = 0 // ✅

これができるのは自然なことに思えるかもしれませんが、 値型 についてはそれほど自然なことではありません。 Computed Property はプロパティのふりをしたメソッドのようなものです。 owner が Computed Property ではなくメソッドだったらと考えると上記のコードで状態を変更できることが不自然に思えてきます。

// owner がメソッドだったら
struct Group {
    func owner() -> User {
        members[0]
    }
}

User値型 の場合、 owner メソッドの戻り値に対して変更を加えようとするとコンパイルエラーになります。

// 値型の場合
group.owner().points = 0 // ⛔

User値型 なので、 group.owner() の戻り値は return される際にコピーして生成されます。生成された User インスタンスは、 group.owner().points = 0 が実行される間、一時的にメモリ上に存在するだけです。一時的な存在の User インスタンスに変更を加えても( points0 にしても)、その結果は破棄されてしまうので意味がありません。そのような無意味なケースを Swift コンパイラはコンパイルエラーとして検出します。

では、 owner が Computed Property の場合には、どうして points の変更結果を group に反映することができるのでしょうか。それは、たとえば group.owner.points = 0 の実行時には次のような手順で処理が行われるからです。

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

group.owner().points = 0 という一つの式が実行される間に owner プロパティの getset がそれぞれ異なるタイミングで呼び出されることによって、 Computed Property を介した状態の変更が実現されているわけです。

inout 引数や mutating func 同様に、これも 値型 の使い勝手を ミュータブルクラス 相当に向上させるために欠かせない言語仕様です。 Userミュータブルクラス であれば、 group.owner.points = 0 ができるのは当たり前です( 参照型 であれば owner プロパティが返すインスタンスは group と共有されているので)。 値型 においてもこれと同じ使い勝手を実現するためには、 Computed Property を介しても変更を反映できる仕組みが必要なわけです。

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

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

これも User値型 のときには、 subscriptgetpoints の変更、 subscriptset の順に実行されます。

さらに、 Computed Property や subscript を介した変更は、 inout 引数や mutating func と組み合わせることで強力に機能します。

// 値型の場合

// inout 引数との組み合わせ
try consumePoints(100, of: &group.owner) // ✅

// mutating func との組み合わせ
try group.owner.consumePoints(100) // ✅

Swift を使っていると当然のようにこれらの恩恵を受けることができますが、他の言語と比較してみると、それがいかに 値型 の使い勝手を向上させているかがわかります。たとえば、 C# ではどのパターンも Swift と同じような挙動にはなりません。

// C#
group.Owner.Points = 0; // コンパイルエラー⛔
ConsumePoints(100, ref group.Owner); // コンパイルエラー⛔
group.Owner.ConsumePoints(100); // コンパイルが通り、実行しても何も起こらない😵

高階関数と inout 引数の組み合わせ

これまでは例として単一の User に対する変更を取り上げてきましたが、ここでは複数の User 、つまり [User] に対する変更を考えてみましょう。

Userミュータブルクラス の場合、特別なことは何もありません。 for-in ループで User を一人ずつ取り出して変更を加えるだけです。 [User] に格納された全 User に 100 ポイントを付与するコードは次のようになります。

// ミュータブルクラスの場合
let users: [User] = ...
for user in users {
    user.points += 100
}

User値型 のとき、これと同じことをやろうとすると一筋縄ではいきません。まず、 for-in ループのループ変数はデフォルトで定数なので、そのままではコンパイルエラーになります。

// 値型の場合
var users: [User] = ...
for user in users {
    user.points += 100 // ⛔
}

だからといってこれを変数にすれば良いわけではありません。 UserValue Semantics を持っているので、ループ変数 user を変更したからといって、それが users に反映されるわけではありません。

// 値型の場合
var users: [User] = ...
for var user in users {
    user.points += 100 // コンパイルが通り、実行しても何も起こらない😵
}

このようなケースでの一般的な対処法は、インデックスを介して状態を変更することでしょう。

// 値型の場合
var users: [User] = ...
for i in users.indices {
    users[i].points += 100 // コンパイルが通り、変更も反映される✅
}

しかし、別の方法もあります。それは、次のような modifyEach メソッドを導入することです。

extension MutableCollection {
    mutating func modifyEach(_ body: (inout Element) -> Void) {
        for i in indices {
            body(&self[i])
        }
    }
}

この modifyEach メソッドを用いると、次のようにして [User] に格納された全 User の状態を変更することができます。

// 値型の場合
var users: [User] = ...
users.modifyEach { user in
    user.points += 100 // コンパイルが通り、変更も反映される✅
}

modifyEach メソッドは引数 body にクロージャを受け取る高階メソッドですが、おもしろいのは bodyinout 引数を持っていることです。上記の例では、 modifyEach に渡すクロージャは引数 user を受け取りますが、これが inout 引数になっており、クロージャの中で user に加えた変更は users に反映されるわけです。

残念ながら modifyEach については swift-evolution で議論されたことはあるものの、標準ライブラリに加えられるには至っていません。しかし、 Swift 4 で追加された reduce(into:_:) という高階メソッド(API リファレンス)は modifyEach と同じように inout 引数との組み合わせを活用しています。

extension Sequence {
    func reduce<Result>(
        into initialResult: Result,
        _: (inout Result, Element) throws -> ()
    ) rethrows -> Result {
        ...
    }
}

従来の reduce メソッドは関数型言語由来で 値型 との相性がよくありませんでした。

let idToUser: [Int: User] = users.reduce([:]) { result, user in
    var result = result
    result[user.id] = user // O(N): ここで Dictionary 全体をコピー😵
    return result
}

reduce(into:_:) の追加により 値型 との親和性が高まりました。

let idToUser: [Int: User] = users.reduce(into: [:]) { result, user in
    result[user.id] = user // O(1) 😄
}

なお、 modifyEach 相当のことは、将来的には Ownership Manifesto の中で示されている for-in ループと inout の組み合わせでできるようになる可能性があります。

// 値型の場合(将来的にできるようになるかもしれないこと)
var users: [User] = ...
for inout user in users {
    user.points += 100 // コンパイルが通り、変更も反映される✅
}

値型とミュータブルクラスの使い分け

これまで、 値型 を使うことで ミュータブルクラスイミュータブルクラス のいいとこ取りができること、 Swift には 値型 の使い勝手を向上する言語仕様が充実していることを見てきました。しかし、 値型 も万能ではありません。すべてを 値型 にすることが必ずしも望ましいわけではなく、 値型 に固執しすぎるとかえってコードを複雑化してしまうでしょう。 値型参照型 を適切に使い分けることが重要です。

参照型 といっても、 Swift において イミュータブルクラス を実装すべきケースはほとんどありません。 イミュータブルクラス がほしくなったら structenum などの 値型 で代替できないかを考えてみるのが良いでしょう。注意が必要なのは、ある機能を持った型を イミュータブルクラス の代わりに struct で実装しようとする場合、必ずしもその structイミュータブル でなくて良いということです。 イミュータブルクラス がほしい理由のほとんどは Value Semantics がほしいことです。多くの言語で Stringイミュータブルクラス ですが、 Swift の Stringミュータブルstruct です。 Swift の String はそれでも問題なく機能しており、それは String に求めらているのが イミュータビリティ ではなく Value Semantics だからです。

問題は 値型ミュータブルクラス をどのように使い分けるかです。これは、言い換えると Value SemanticsReference Semantics をどのように使い分けるかという問題です。 “Value Semantics とは” で見たように、 Value SemanticsReference Semantics も持たない型は使い勝手が良くありません。また、 Value SemanticsReference Semantics を兼ね備えた イミュータブル な型を作る場合、基本的には 値型 にすれば問題ありません。そう考えると、 値型ミュータブルクラス かの選択というのは、 ミュータブル な型に Value Semantics を持たせるのか Reference Semantics を持たせるのかの選択だと言えます。

そのような場合にどちらが望ましいかはケース・バイ・ケースで、一概に線引きをするのは困難です。判断に迷った場合に筆者がおすすめする方法は、まず 値型 × Value Semantics で実装し、それでうまくいかない場合に ミュータブルクラス × Reference Semantics を検討するというものです。 Reference Semantics の利用を最小限に留めることで、「意図しない変更」や「整合性の破壊」などの問題の発生機会を最小化し、問題のコントロールが容易になります。

そのときに、 値型 × Value Semantics の世界と、 参照型 × Reference Semantics の世界を分離しておくことが重要です。 Value Semantics の世界では Reference Semantics の問題を気にする必要がありません。 Value Semantics の世界をどれだけ広げられるかというのが、設計における一つのポイントになるでしょう。これは、 参照型 中心の言語で イミュータブル な世界を分離して、できるだけ広げる努力をするのと本質的に同じことです。しかし、 値型 には イミュータブルクラス よりも状態の変更が容易であるという性質があります。そのため、 Value Semantics と変更の煩雑さとのバランスを取る際に、より積極的に Value Semantics を採用しやすいでしょう。

まとめ

値型ミュータブルクラスイミュータブルクラス のいいとこ取りをした存在であり、 Swift はその 値型 を中心とした言語です。 値型 中心の言語はめずらしいですが Swift でそれが可能だったのは、 Copy-on-Write を用いて効率の良い 値型 コレクションを実現できたことと、 値型 の使い勝手を向上させる言語仕様によって 値型 中心のコードで問題になりがちな点をカバーしたことによると筆者は考えています。 Swift は進化の途上であり、 for-in ループと inout の組み合わせなど、今後さらに 値型 の使い勝手が改善されていくものと思われます。

ただし、 値型 も万能ではありません。 値型 × Value Semanticsミュータブルクラス × Reference Semantics を適切に使い分ける必要があります。どちらを採用すべきかわからないときは、まず 値型 × Value Semantics で始め、問題が生じたら 参照型 × Reference Semantics を検討することをおすすめします。