今回はタイトルにもある通り、値型と参照型について説明していきます。
プログラミングの基礎的な概念だと思うので、理解しといて損はありません。
今回はSwiftで説明していきたいと思います。
値型
まずは値型について説明します。
値型は変数の中に実際の値が格納されます。正しく言うと、変数の領域に格納されます。
今回は値型であるstruct
(構造体)で見てきたいと思います。
struct Human { var rank: UInt8 var age: UInt8 }
上記のような構造体があるとします。説明しやすいように変数はUInt8
にしています。
構造体(値型)の場合は値の全てがスタック領域に確保されます。
var kuma = Human(rank:1, age:23)
なので、上記のような宣言をした場合は 1 バイトのUInt8
を二つ持っているので2バイト分(Human)の領域が確保されます。
2バイトの領域の最初の1 バイトには1が、次の 1 バイトには23が格納されるイメージです。イメージは下記です。
では、次の場合はどうなるでしょう?
var kuma = Human(rank:1, age:23) var kumaCopy = kuma
kuma
という構造体をkumaCopy
という変数に代入しています。
次のようなイメージになります。
代入した変数はスタックに確保されるのですが、新しく確保された領域にコピーされた値が格納されます。 また、値型の場合変数には値そのものが入ります。
そのためコピー元の値を変更しても代入先の値は変わりません。
var kuma = Human(rank:1, age:23) var kumaCopy = kuma kuma.rank = 2 print(kumaCopy.rank)
上記のprintでは1が表示されます。
値型の場合スタックに値がコピーされ続けるのでデータ量の多い構造体の場合メモリを大きく食いつぶす可能性があります。
スタック領域は容量が小さいのですぐ使い切ってしまいます。
この場合スタックを使い切ってしまったエラーをスタックオーバーフローといい、クラッシュします。
参照型
参照型は別の場所に値を格納し、そのアドレス(どこに値を格納したか)を変数に格納します。
値型はスタック領域に格納されていましたが、参照型はヒープ領域に格納されます。
参照型ではクラスを元にして説明します。
class Human { var rank: UInt8 var age: UInt8 init(rank: UInt8, age: UInt8) { self.rank = rank; self.age = age } }
以下のようにして、オブジェクトを生成し変数に格納します。
var kuma = Human(rank:1, age:23)
クラス(参照型)はヒープ領域に確保されますが、その確保のされ方は値型がスタックに確保される時と同じです。
違うのはHuman(rank:1, age:23)
が返す値が生成された領域の先頭アドレスということだけです。
つまり、kumaにはオブジェクトのアドレスが格納されています。
イメージは下記です。
このように参照型は値そのものではなく、オブジェクトへの参照を変数に格納します。
すると、値型で試した変数の代入を行った場合はどうなるでしょうか?
var kuma = Human(rank:1, age:23) var kumaCopy = kuma
先にイメージをもとに説明したいと思います。
変数はスタックに確保され、変数宣言をすると新しくスタック領域に値のコピーが作成されます。
参照型の場合は変数にはアドレスという値がコピーされます。
指し示すアドレスは同じオブジェクトなのでkumaとkumaCopyは同じものとみなすことができます。
なので、コピー元の値を変更すると代入先の値も変更されます。
var kuma = Human(rank:1, age:23) var kumaCopy = kuma kuma.rank = 2 print(kumaCopy.rank)
上記のprintでは2が表示されます。
これは意図せず値が変わってしまう可能性があるので注意が必要です。
struct(値型)とclass(参照型)はどちらを使うのがいいのか?
ここまで値型と参照型を見てきて違いがわかったと思いますが、どちらを使った方がいいのかと思った方もいると思います。
個人的には、class(参照型)がメインで場合によってはstruct(値型)がいいと思います。
まずは、参照型からですが、参照型は代入のパフォーマンスだと思います。
var kuma = Human(rank:1, age:23) var kumaCopy = kuma
上記は参照型の例なので、Human
はクラスです。
kumaCopy
はkuma
のメモリの先頭をコピーするだけです。32 bit アーキテクチャなら 4 バイト、 64 bit アーキテクチャなら 8 バイトのデータだけになります。
一方、値型の場合は値を全てコピーします。
値型で説明したときはrank
とage
だけでしたが、下記のようにより多くのデータがある場合はその分コピーしないといけません。
struct Human { var data1: UInt8 var data2: UInt8 var data3: UInt8 var data4: UInt8 // 省略 var data100: UInt8 }
上記のように変数が100個あると100個分コピーしないといけなくなり、その分の領域を確保しなくてはいけなくなりパフォーマンスが悪化する可能性があります。
structを使うのは比較的単純な値を扱うときのみの方がいい気がしました。
また、struct
は継承ができないため、継承する場合はclass一択です。
最後に、Swiftの標準ライブラリで提供される型(IntやString、配列など)はほぼすべてが値型という珍しい言語です。
メモリ領域
最後にメモリ領域について軽く述べたいと思います。
今まででスタック領域やヒープ領域などいきなり説明の中に出てきたと思います。軽く解説します。
スタック領域はメモリがA→B→Cの順で確保された場合、C→B→Aの順で解放される。メモリをいわゆるスタックで管理している。 処理ブロック({}で囲まれたブロック)を抜けると解放されます。 ヒープよりも割り当てられている領域が小さい。
ヒープ領域は確保されたメモリは明示的に解放しないとずっと留まり続ける。そのため気をつけていなければメモリリークを引き起こしクラッシュの原因になる。 自由なサイズのメモリを確保できます。
ヒープ領域の場合は、メモリを使い終わったら開放しないといけないのですが、Swiftでは自動的に解放されるようになっています。
この自動で解放してくれる仕組みをガベージコレクションと呼んでいます。 ガベージコレクションではヒープ領域に対して参照カウンタというものを持ちます。 参照カウンタとは、何個の変数から参照されているのかカウントするものです。
var kuma = Human(rank:1, age:23) var kumaCopy = kuma
例えば、上記ではkumaとkumaCopyの2つでHumanは参照されています。 なので、参照カウンタは2になります。
この参照カウンタが0になったタイミングでオブジェクトは解放されます。どのようにするのでしょう?
kumaCopy = nil
nilはどこのアドレスも指していないという意味です。 そうすると参照カウンタが-1され1となります。
ただ、毎回毎回nilにする必要はありません。 変数は処理ブロックを抜けると解放されます。
つまり{}(ブロック)を抜けると変数は解放され、参照カウントが自動的に-1されるので、意識する必要がないです。