post Image
Swift の class の mutating func とは何か

導入

Swift には struct と class があります。 struct のメソッドには func ( nonmutating func の省略表記 ) と mutating func がありますが、 class には func しかありません。実は、 protocol を使うと class においても mutating func を定義することができます。この記事ではこの class における mutating func が何を意味するのか解説します。

バージョン

$ swift --version
Apple Swift version 4.0 (swiftlang-900.0.59 clang-900.0.34.2)
Target: x86_64-apple-macosx10.9

結論

mutating funcselfinout 渡しであること、 class 型の var が参照の可変性を表すことから、メソッド呼び出しのレシーバの変数の参照の可変性を意味するとわかる。

class に mutating func を定義する

class に mutating func を書いても次のようなコンパイルエラーになります。

error: 'mutating' isn't valid on methods in classes or class-bound protocols

一方、 protocol を定義するときにはメソッド制約として mutating func を定義できます。そして struct がそれを実装するときには funcmutating func のどちらかを使えます。しかし class は mutating func が定義できないので、 func で実装することになります。実はここで protocol のデフォルト実装を使うことで、 mutating func なメソッドをクラスに持たせることが可能です。

下記は mutating func のデフォルト実装を与えた例です。 protocol P0 には変更した値を返す版の appended と自己変更版の append があり、 append のデフォルト実装を、 appended を用いて書いています。mutating func の実装なので、 self を左辺に置いた代入文を書くことができます。これは class のメソッドではできないことですが、ここでは protocol なので可能です。

protocol P0 {
    func appended(_ x: Int) -> Self
    mutating func append(_ x: Int)
}

extension P0 {
    mutating func append(_ x: Int) {
        self = appended(x)
    }
}

この protocol を適当な class で conform します。 class 名は Cat にしました。デフォルト実装があるので、 appended メソッドだけ実装することで、 mutating funcappend メソッドを持った class を作ることができます。

final class Cat : P0 {
    var age: Int = 1

    func appended(_ x: Int) -> Cat {
        let cat = Cat()
        cat.age += x
        return cat
    }
}

class の mutating func を呼び出す

さて、この class のインスタンスに対してこれらのメソッドを呼び出そうとして、以下のようなコードを書いてもコンパイルエラーになってしまいます。

func main() {
    let cat = Cat()
    cat.append(1) // compile error
}

エラーはこれです。

error: cannot use mutating member on immutable value: 'cat' is a 'let' constant

let の mutating member が使えないと言われています。そこで letvar に書き換えると動くようになります。ついでに、メソッド呼び出しの前後で、 cat.age の値と、 catObjectIdentifier を表示しておきます。

func main() {
    var cat = Cat()
    print(ObjectIdentifier(cat), cat.age)
    cat.append(1)
    print(ObjectIdentifier(cat), cat.age)
}

出力はこれです。

ObjectIdentifier(0x000060c000025280) 0
ObjectIdentifier(0x000060c000024f80) 1

age は期待通り 0 から 1 になっていますが、 ObjectIdentifier の値も変化しています。つまり、 メソッド呼び出しによって、メソッドのレシーバの変数が別のインスタンスを参照するように変化した ことがわかります。これが class における mutating func の意味です。

mutating funcselfinout 渡しする

上記の挙動は一見不思議ですが、 関連する概念を整理するとスッキリします。

ポイントは mutating funcselfinout 渡しするということです。(Ownership Manifesto#Function Parameters)

インスタンスメソッドというのは、レシーバのインスタンスも引数の一つと考えると、通常の関数としてとらえることができます。例えば、先程でてきた nonmutating func である appended メソッドは以下のように考えることができます。1 self を関数の引数として渡しています。

func P0_appended<X: P0>(_ self: X, _ x: Int) -> X { ... }

これが mutating func の場合は selfinout 渡しになります。 inout 渡しというのは、引数の型に inout が付くということです。 つまり、 self の型が inout X になります。 append を例にするとこうです。

func P0_append<X: P0>(_ self: inout X, _ x: Int) { ... }

この形式で class の mutating func を捉え直すことでわかりやすくなります。先程の Cat クラスにおける append メソッドは以下の関数のようにとらえられます。

func Cat_append(_ self: inout Cat, _ x: Int) { ... }

そして、 以下の cat に対するメソッド呼び出しは、次の関数呼び出しとしてとらえられます。

// メソッド呼び出し形式
cat.append(1)

// 関数呼び出し形式
Cat_append(&cat, 1)

後者の形式を見ると、 catvar でなければならないこと、そして、関数呼び出しによって、 cat が指し示す値が、別のインスタンスに変わるかもしれないことがわかります。これこそが、 class の mutating func の意味となります。


  1. 本当は nonmutating funcselfshared 渡しのため不正確な記述ですが、現状の Swift に明示的な shared 渡しが存在しないのでこうしました。 


『 Swift 』Article List