Swiftで値型と参照型の違いを理解する
↓の挙動の違いの理由を説明できますか?
struct (値型)の場合
struct Foo {
var a: UInt8
var b: UInt8
}
let foo = Foo(a: 3, b: 127)
foo.a = 5 // コンパイルエラー!!
class (参照型)の場合
class Bar {
var a: UInt8
var b: UInt8
init(a: UInt8, b: UInt8) { self.a = a; self.b = b }
}
let bar = Bar(a: 3, b: 127)
bar.a = 5 // OK
ことのはじめ
仕事でコードレビューをしていて、次のようなコードを見かけました( Bar
は↑の Bar
とします)。
var bar = Bar(a: 2, b: 3)
...
bar.a = 5
「どうして let
じゃなくて var
にしたの?」と聞いてみたところ、「 bar
を変更するから let
じゃいけないと思った」という返事が返ってきました。値型と参照型を混同してしまっているようです。
そういうわけで、値型と参照型の違いについて説明することにしました。
値型の場合
変数を定義すると、その変数の値を格納するためにメモリ上に領域が確保されます。上記の Foo
であれば、 1 バイトの UInt8
を二つ持っているので 2 バイト分の領域が確保されます。
let foo = Foo(a: 3, b: 127)
とすると、その 2 バイトの領域の最初の 1 バイトには 3
が、次の 1 バイトには 127
が格納され、 16 進数で書くと 037F
という 2 バイトが格納されることになります。
# メモリ上のバイト列のイメージ
| | 03 | 7F | | |
^ ^
foo のための領域
let
とは、その変数のために確保された領域を、後から変更できないようにするという意味の宣言です。 037F
という値は、変数 foo
が存在している限り変更されません。
foo.a = 5
を実行するためには 037F
を変更して 057F
に書き換えなければなりません。それは let
に違反するのでコンパイルエラーとなります。
# メモリ上のバイト列のイメージ
| | 03 | 7F | | | |
^
ここを 05 に書き換えないといけない
参照型の場合
参照型とは何か
値型が値を直接変数の領域に格納するのに対して、参照型では別の場所に値を格納し、そのアドレス(どこに値を格納したか)を変数に格納します。
let bar = Bar(a: 3, b: 127)
とすると、メモリ上のどこか(変数のための領域とは別の場所)に 037F
という 2 バイトのデータが格納されます。ここではメモリ上の 7FAB52C4 番地からの 2 バイトに格納されたとしましょう。 7FAB52C4 番地には 03
が、 7FAB52C5 番地には 7F
が格納されます。
では、変数 bar
のために確保された領域には何が書かれるのでしょうか。それは 7FAB52C4
という値(アドレス、番地)です1。
# メモリ上のバイト列のイメージ
| | 7F | AB | 52 | C4 | ... | 03 | 7F | |
^ ^ ^ ^ ^
bar のための領域 7FAB52C4番地
println(bar.a)
とすると何が起こるでしょうか。 Bar
は参照型なので、まず bar
に格納された値 7FAB52C4
が読み出されます。次に、そのアドレス 7FAB52C4 番地にアクセスして、 03
という値が読み出されます。その結果 3
という値が取得され表示されます。
このように、参照型ではアドレスを介して間接的に値にアクセスすることになります。
参照型と let
参照型である Bar
型の変数 bar
を let
にする意味は、 bar
に格納された 7FAB52C4
を変更できないようにするという意味です。
もし、
bar = Bar(a: 5, b: 255)
とするとどうなるでしょうか。 Bar(a: 5, b: 255)
は新たにメモリ上のどこか(ここでは 8B3E4CA9 番地としましょう)に 2 バイトの領域を確保し、そこに値 05FF
を書き込みます。そして、 bar
に格納された 7FAB52C4
を 8B3E4CA9
に書き換えようとします。しかし、これは bar
が let
で宣言されていることに違反するのでコンパイルエラーとなります。
# メモリ上のバイト列のイメージ
| | 7F | AB | 52 | C4 | ... | 03 | 7F | ... | 05 | FF | |
^ ^ ^ ^ ^ ^
8B 3E 4C A9 に書き換えたい 7FAB52C4番地 8B3E4CA9番地
しかし、
bar.a = 5
としても、 7FAB52C4 番地に格納された 03
から 05
に書き換えるだけで、 bar
に格納された 7FAB52C4
を書き換えるわけではありません。そのため、 bar
が let
で宣言されていても bar.a = 5
は許されるのです。
# メモリ上のバイト列のイメージ
bar
| | 7F | AB | 52 | C4 | ... | 05 | 7F | |
^ ^ ^ ^ ^
ここはそのまま ここを 03 から 05 に書き換えた
なお、 7FAB52C4
番地の値が書き換えられるかどうかは、 a
が var
で宣言されているか let
で宣言されているかによります。今は var
なので書き換えられますが、 let
で宣言されていると bar.a = 5
はコンパイルエラーとなります。
参照型がうれしい理由
参照型に慣れてない人にとっては、一見参照型は複雑なだけで何がうれしいのかわかりづらいと思います。参照型がうれしい理由の一つは代入のコストが小さいことです。
代入とは、代入先の変数のために確保された領域に、値をまるごとコピーする操作です。
let foo1 = Foo(a: 3, b: 127)
let foo2 = foo1
とすると、 foo1
と foo2
のために 2 バイトずつの領域が確保されます。最初の行で foo1
の領域には 037F
が格納されます。次の行で、 foo2
の 2 バイトの領域に、 foo1
の領域に格納されている 037F
をまるごとコピーします。
# メモリ上のバイト列のイメージ
| | 03 | 7F | | ... | | 03 | 7F | |
^ ^ ^ ^
foo1 のための領域 foo2 のための領域に 03 7F をコピー
では、もし次のような struct
だとどうなるでしょうか。
struct Baz {
let v1: UInt8
let v2: UInt8
let v3: UInt8
...
let v100: UInt8
}
let baz1 = Baz(...)
let baz2 = baz1
Baz
型の変数を宣言すると 100 バイトの領域が確保されます。 baz1
から baz2
へ代入すると、 100 バイト分もデータをコピーしないといけません。
プログラミングをしているともっと大きなデータを扱わなければならないことがあります。例えば、配列で 100 万個の要素が必要なこともあるでしょう。その代入(引数に渡したり、戻り値で受け取ったりも含みます)の度にすべてをコピーしていると、プログラムのパフォーマンスはひどいことになってしまいます。
では、参照型だとどうでしょうか。
class Qux {
let v1: UInt8
let v2: UInt8
let v3: UInt8
...
let v1000: UInt8
init(...)
}
let qux1 = Qux(...)
let qux2 = qux1
この場合、 qux1
から qux2
にコピーされるのは qux1
に格納されたアドレスの値だけです。アドレスは、 32 bit アーキテクチャなら 4 バイト、 64 bit アーキテクチャなら 8 バイトのデータです。 qux1
から qux2
への代入は、たとえそのアドレスが参照する値がどれだけ巨大でも、わずか 4 バイト(または 8 バイト)のコピーで済みます。
# メモリ上のバイト列のイメージ
| 7F | AB | 52 | C4 | ... | 7F | AB | 52 | C4 | |
^ ^ ^ ^ ^ ^
qux1 のための領域 qux2 のための領域に 7F AB 52 C4 をコピー
参照型の代償(または、参照型がうれしい理由その2)
参照型は、次のような現象を引き起こします。
// 値型の場合
var foo1 = Foo(a: 3, b: 127)
var foo2 = foo1
foo2.a = 5
println(foo1.a) // 3 ( foo2 を変更しても foo1 は変更されない)
// 参照型の場合
var bar1 = Bar(a: 3, b: 127)
var bar2 = bar1
bar2.a = 5
println(bar1.a) // 5 ( bar2 を変更したら bar1 も変更された)
これは、 bar1
と bar2
には同じアドレスの値が格納されており、その実体となる値はメモリ上に一つしか存在しないためです。
例えば、 bar1
と bar2
に格納されている値(アドレス)が 098E6F1A
だったとします。すると、 2 行目が終了した時点では 098E6F1A 番地からの 2 バイトには 037F
が格納されているはずです。次に 3 行目で bar2.a = 5
が実行されると、 098E6F1A 番地からの 2 バイトは 057F
に書き換えられてしまいます。 bar1
も同じ 098E6F1A 番地を参照しているので、 bar1.a
も 5
になってしまうわけです。
# メモリ上のバイト列のイメージ
# bar2.a = 5 の実行前
| 09 | 8E | 6F | 1A | ... | 09 | 8E | 6F | 1A | ... | 03 | 7F | |
^ ^ ^ ^ ^ ^ ^ ^ ^
bar1 のための領域 bar2 のための領域 098E6F1A 番地
# bar2.a = 5 の実行後
| 09 | 8E | 6F | 1A | ... | 09 | 8E | 6F | 1A | ... | 05 | 7F | |
^ ^ ^ ^ ^ ^ ^ ^ ^
bar1 のための領域 bar2 のための領域 05 に書き換えた
このような挙動は、参照型に慣れていない人にとってはわかりづらく感じられるでしょうし、参照型の挙動を正しく理解していないと思わぬバグを生む可能性があります。
ただし、参照型のそのような挙動は悪いことだとは限りません。プログラム全体で状態を共有して、ある箇所で値を書き換えたら他の箇所にも反映されてほしいということもあります。大切なのは、値型と参照型の違いを理解して、正しく使い分けることです。
-
エンディアンについては本題とは関係なく話が複雑になるだけなので意図的に無視しています。 ↩