導入
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 func
が self
の inout
渡しであること、 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 がそれを実装するときには func
か mutating 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 func
な append
メソッドを持った 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 が使えないと言われています。そこで let
を var
に書き換えると動くようになります。ついでに、メソッド呼び出しの前後で、 cat.age
の値と、 cat
の ObjectIdentifier
を表示しておきます。
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 func
は self
を inout
渡しする
上記の挙動は一見不思議ですが、 関連する概念を整理するとスッキリします。
ポイントは mutating func
は self
を inout
渡しするということです。(Ownership Manifesto#Function Parameters)
インスタンスメソッドというのは、レシーバのインスタンスも引数の一つと考えると、通常の関数としてとらえることができます。例えば、先程でてきた nonmutating func
である appended
メソッドは以下のように考えることができます。1 self
を関数の引数として渡しています。
func P0_appended<X: P0>(_ self: X, _ x: Int) -> X { ... }
これが mutating func
の場合は self
が inout
渡しになります。 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)
後者の形式を見ると、 cat
が var
でなければならないこと、そして、関数呼び出しによって、 cat
が指し示す値が、別のインスタンスに変わるかもしれないことがわかります。これこそが、 class の mutating func
の意味となります。
-
本当は
nonmutating func
のself
はshared
渡しのため不正確な記述ですが、現状の Swift に明示的なshared
渡しが存在しないのでこうしました。 ↩