🐕

改めて 参照型 と 値型 を考える(例:C#)

2023/02/20に公開

目次

値型と参照型

なぜ値型と参照型という2種類のデータ型が存在するのか

一言でいうと、「システム動作時に利用するメモリの管理を最適化する」ため。

基本的にシステムは、メモリの中に電気信号からなるデータを入れたり出したりを繰り返すことによって動いている。そして、入れたり出したりするデータ量が増えれば増えるほど処理に時間がかかり、システムの動作が遅くなる。
よってシステムを速く動かすためにはなるべくメモリへのデータの出し入れを少なくする必要がある。
データの出し入れを効率化するために2種類のデータ型を用意して、効率よくメモリの管理ができるように設計されている。

値型(Value Type)とは

値型の挙動

変数に実際のデータを直接入れるデータ型。

int a = 5;
int b = a;

このコードの場合
変数aの中に5という値が入っており、変数bの中にも5という値が入っている。
このa,bという2つの変数には、それぞれ別のメモリ空間に5という別のデータが入っている。
aの中にある5というデータを複製してbに入れるということである。

値型の種類

int型、double型、char型、bool型、DateTime型などがある。
これらの型にはメモリの上限が決められている。
下記がデータ型とメモリ使用量の上限の関係である。

データ型 メモリ使用量の上限
int(Int32) 32ビット(4バイト)
long(Int64) 64ビット(8バイト)
double 64ビット(8バイト)
decimal 128ビット(16バイト)
char 16ビット(2バイト)
bool 8ビット(1バイト)
byte 8ビット(1バイト)
DateTime 8バイト~16バイト

上限が決まっているということは、その変数の中のデータの変更は上限メモリの範囲内になる。
あまりに大きなメモリを使用したり、大きくデータ容量が可変する場合は、後述する参照型を利用して対応する。
Int32型は、2(1bit)の32乗の範囲の数値が利用できる(32bit分のデータが保存可能)。

値型とスタック領域

スタック領域とは、値型のデータを格納するメモリ領域のことである。
主に関数の呼び出し時に使われるメモリ領域で、関数が呼び出されたときに、関数の実行に必要な情報を一時的に保存するために使用される。

static int Add(int a, int b)
{
    int result = a + b;
    return result;
}

static void Main()
{
    int x = 1;
    int y = 2;
    int z = Add(x, y);
    Console.WriteLine(z);
}

このコードに対してのChatGPTの解説が下記である。

このコードでは、 Add 関数を定義し、 Main 関数から呼び出しています。 Add 関数は、引数の a と b を足して、その結果をローカル変数の result に代入し、 result を戻り値として返します。 Main 関数では、変数 x と y に値を代入し、 Add 関数に渡して、戻り値を変数 z に代入しています。

この場合、関数呼び出し時には、 Add 関数の引数 a と b 、およびローカル変数 result が、スタック領域に確保されます。そして、 Add 関数が終了すると、それらの変数はスタック領域から削除されます。

具体的には、関数呼び出し時に、引数 a と b がスタック領域に積まれます。そして、ローカル変数 result が宣言されたときにも、その変数がスタック領域に積まれます。次に、 result に計算結果が代入され、return文によって、戻り値がスタック領域から取り出されて Main 関数に返されます。

つまり、 Add 関数の呼び出し中に使用される変数は、スタック領域に確保され、関数呼び出しが終了すると、スタック領域から自動的に削除されるため、メモリの解放についてはプログラマが意識する必要はありません。

つまり、関数内で値型の変数が宣言されたときに、宣言された型の上限メモリ量がスタック領域に確保され利用される。
その関数を抜けたときに、使用されているメモリは消滅する(ように C# で設計されている)。

スタック領域の上限はアプリケーションによって決まっている。例えばWindowsの場合のスタック領域上限はデフォルトで1MB。Linuxの場合は8MB。
この上限を超えた場合は、スタックオーバーフローエラーが発生してシステムが止まる。

参照型(Reference Type)とは

参照型の挙動

参照型の場合は、変数に格納するものは、オブジェクトの場所だけである。この場所のことをポインタとかアドレスとか表現する。

int[] a = { 1, 2, 3 };
int[] b = a;
b[0] = 5;

この場合 a[0] も b[0] も中身は5になっている。a[0] には何も入れていないはずなのにデータが変更されている。
これは a と b の変数がどちらも同じポインタを参照しているからである。同じ場所を参照していることで、片方のデータを変えたらもう片方も変更される。

親の銀行口座から私がお金をおろしたら、親からしたら知らないうちに自分のお金が減っている。結局参照している銀行口座は一つで、どこから誰がお金を下ろしても、その出金が反映されるのと似ている(ほんとか?)

参照型の種類

object型、string型、array型、List<T>型、class型、interface型、delegate型などがある。
これら参照型の型は、値型と比べ多くのデータを格納することができる。しかしデータ量が多い場合、値型ようにデータをコピーすると問題が起こる。具体的には参照型のデータを変数に代入するたびに、多量のデータをすべてコピーすることになる。これではメモリへのデータの出し入れが毎回膨大になってしまい、システムの動作速度が遅くなってしまう。
よって、データ量の大きくなるまとまりは参照型として、ポインタのみを変数に格納するように設計されている。

参照型とヒープ領域とスタック領域

参照型のオブジェクトは、インスタンス化されたときにその実体はヒープ領域という場所に格納される。そしてそのインスタンスのポインタをスタック領域として変数が保存する。
つまり参照型のインスタンスを変数に格納する場合、スタック領域とヒープ領域どちらのメモリ領域も使用される。

ヒープ領域とガベージコレクションとスタック領域

スタック領域のメモリ空間は、該当の関数のスコープを抜けたときに解放される。しかしヒープ領域に生成されたメモリ空間は明示的にメモリの解放をしないと残り続ける。それではメモリ空間がどんどんと圧迫されてしまう。これを解決するためにガベージコレクションという仕組みがある。
ガベージコレクションはヒープ領域に確保されたメモリ領域を監視しており、参照されていないオブジェクトがあれば自動でメモリの解放をしてくれる。

C言語ではスタック領域やヒープ領域を直接操作することができるが、C#の場合は.NET Frameworkに実装されている上述のメモリ管理の処理が自動でメモリの解放を行う。

まとめ

C# に存在する型には、値型と参照型がある。これらのインスタンスはそれぞれデータが保存されているメモリ空間が異なる。またメモリの解放の仕方も異なる。
これらはメモリの管理を最適化してパフォーマンスが落ちないために設計されている。


図:実戦で役立つ-C-プログラミングのイディオム-定石-パターンp29 より引用

Discussion