Swift の配列で append すると変数が参照する値まで変わる話のしょぼいメモ

Swift の Array の append の振る舞いが直感に反する的な意味で話題になってて、自分も面白かったので、調べたメモ。

一応注意:調べ方が雑なのでウソ書いてるかもしれないので注意してください。

話題のSwiftコード

var a = [1, 2, 3]
var b = a

println(a) // [1, 2, 3]
println(b) // [1, 2, 3]
a.append(4)
println(a) // [1, 2, 3, 4]
println(b) // [1, 2, 3] <= b はいったい何を見ているんだ…?

Rubyならこうなる。大体のOOPLならこうなると思う。

a = [1, 2, 3]
b = a

p a # => [1, 2, 3]
p b # => [1, 2, 3]
a.push(4)
p a # => [1, 2, 3, 4]
p b # => [1, 2, 3, 4]


append で、破壊せずに新しい配列を作ったからだよって話だけど、変数aが見てるものを勝手にappendが変えても良い訳ないだろと思って調べた。

クラスと構造体

Swiftには インスタンスを定義するものとして Class と Structure と Enumeration があるけど、 Class から生成されたオブジェクトは変数に対して参照を渡すのに対して、 Structure と Enumeration は value type と呼ばれ、変数にアサインされた時点で値を渡すという前提があって、 Swift の Array と Dictionary は Structure で構成されている。

all of the basic types in Swift―integers, floating-point numbers, Booleans, strings, arrays and dictionaries―are value types

副作用のある操作

構造体は変数へのアサインや破壊的操作(副作用のある操作)をやったときにはコピーを作る。仕様的にはメンバ変数を上書きする(副作用を及ぼす)メソッドには mutating というキーワードが必要。
配列の場合は、長さが変わるメソッドを呼んだときにコピーが作成されるので、 append した時点で a と b は違うものを見ている。そして、 value type は自分自身と同じ型ならコピーで自分を書き換えられる(self = と出来る)ので、変数が参照しているオブジェクトを代入無しに書き換えているように見える。ということみたい。

小並感

Cの構造体における代入時の仕様とほとんど同じだけど、mutating があることで副作用を明示的に指定でき、コンパイラのチェックを受けることが出来る。同じ仕組みを普通のオブジェクトに導入してないのが面白い。ネイティブアプリ用の言語だから性能要求や下位レイヤーへの親和性を保たせつつ、OOPLっぽい仕様としたかったのかな。言語仕様業界に詳しくないので分からないけど。

実験

mutating を付けていないところで、メンバ変数を上書きしようとするとコンパイルエラーである。

$ cat kero.swift
struct Aaa {
  var a: Int

  func aaa() {
    self.a = 1
  }
}
$ xcrun swift kero.swift
kero.swift:6:12: error: cannot assign to 'a' in 'self'
    self.a = 1
    ~~~~~~ ^

mutating 付けると通った。

$ cat kero.swift
struct Aaa {
  var a: Int

  mutating func aaa() {
    self.a = 1
  }
}
$ xcrun swift kero.swift

チンプイがワンダユウさんに変身するコードで実験してみた。

struct Chinpui {
  var namae: String

  func name() -> String {
    return self.namae
  }

  mutating func chinpui() {
    self = Chinpui(namae: "wandayu")
  }
}

var dare1 = Chinpui(namae: "chinpui")
var dare2 = dare1
println("dare1: \(dare1.name())")
println("dare2: \(dare2.name())")
dare1.chinpui()
println("dare1: \(dare1.name())")
println("dare2: \(dare2.name())")

実行結果

dare1: chinpui
dare2: chinpui
dare1: wandayu
dare2: chinpui
最終的によく分からない

最終的に配列の要素の修正でコピーが発生しない理由(というか仕様の所在)だけが分からず、深夜になったので終了。[]って特別扱い?
コピー発生のタイミングを知りたいけど、ポインタの取り方か object id の取り方が分からず調べ切れていない。