C# Tips
値型がスタック領域に置かれない話
(スタックとヒープ)
C#について学んでいると、値型と参照型の違いについての説明で、よく以下のような説明を見かけます。
構造体は値型で、クラスは参照型。
構造体は値型なので、メモリ上ではスタック領域に確保されます。
クラスは参照型なので、メモリ上ではヒープ領域に確保されます。
メモリとかハードウェアの細かい部分をよく理解していない私は、こうした説明を表面的に理解したつもりで
「ふーん、構造体はスタック領域に置かれるのか」と単純に思い込んでいました。
前述のように説明されているページでも、よく読めば必ずしもそうではないことがちゃんと説明されていたりするのですが、自分の場合は理解があまりにも大雑把すぎて、長らく勘違いしたままでいました。
正しくは、「値型/構造体はスタック領域に置かれる――メソッドの処理スコープ内で宣言された一時的な変数である限りは」ということのようです。
見る人が見れば「何当たり前のこと言ってんの?」って感じなのかもしれませんが、自分はこの基本的なことがわかっていませんでした。
値型/構造体は、配列の要素であろうがクラスのメンバーであろうが何であろうが、常にスタック領域に置かれると思い込んでいたのです。
値型 = スタック領域にある と直接結びつけてしまっていたわけですが、正しくは
値型 = のメソッド内のローカル変数 = スタック領域にある というわけdうえn。
(※以下は独学に基づく理解です。実は間違っている可能性とかあります)
スタック領域に作られるもの
値型/構造体のメソッドローカル変数
参照型の実体への参照のメソッドローカル変数(参照型の実体はヒープ領域に作られる)
newではなくstackallocで宣言されたメソッドローカル配列
ヒープ領域に作られるもの
クラスのメンバー変数(値型/参照型、static/非static問わず)
配列の中身(値型/参照型問わず)
つまり一時的でないもの全部
ということらしいです。
値型であってもスタック領域に作られるものは一時処理用のものだけで、それ以外は値型であろうと参照型であろうと、大抵はヒープ領域に収まっていることが多いはず。
というだけの話でした。
// ※以下は実験して確かめたものではなく、おそらくこうであろうという推測に基づくものです
// 最適化によるコンパイル後の変更なども考えないものとします
// クラス(参照型)
class MyClass
{
// ヒープ領域の予約(参照型であるクラスのメンバーなので)
public int xf;
// ヒープ領域の予約(参照型であるクラスのメンバーなので)
public MyStruct sf;
}
// 構造体(値型)
struct MyStruct
{
// MyStructが宣言された領域に一緒に置かれることになるので、
// ヒープ領域の変数になるかスタック領域の変数になるかはまだ未定
public int af;
}
class App
{
void Start()
{
int x = 1; // スタック領域に作られたxに1が格納される
{
int y = 2; // スタック領域に作られたyに2が格納され、xの上に積まれる
}
// スコープを抜けたので yはスタックから消される
int[] arrayXs = new int[1]; // ヒープ領域に配列が作られ、参照がスタック領域のarrayXsに格納される)
arrayXs[0] = x; // スタック領域からxがヒープ領域の配列領域にコピーされる
int x1 = arrayXs[0]; // ヒープ領域の配列領域から、スタック領域にコピーされる
MyStruct s = new MyStruct(); // スタック領域に構造体が作られる
s.af = x; // スタック領域のxがスタック領域のs.afにコピーされる
MyClass c1 = new MyClass(); // ヒープ領域にクラスが作られ、参照はスタック領域のcに格納される)
c1.xf = x; // スタック領域のxがヒープ領域のc.xfにコピーされる
c1.sf = s; // スタック領域のsがヒープ領域のc.sfにコピーされる
MyClass c2 = null; // どこも示していない参照がスタック領域のc1に格納される
MyStruct[] array_s = new MyStruct[10]; // ヒープ領域に配列が作られ、参照がスタック領域のarray_sに格納される
Span<MyStruct> stack_s = stackalloc MyStruct[10]; // スタック領域に配列が作られる
int z = MyFunc(x); // スタック領域のzに戻り値がコピーされる
}
// メソッド
int MyFunc( int value ) // スタック領域に引数のコピー value が作られる
{
return value + 1; // 値 value+1 が作られて返される
}
}
newの代わりにstackallocを使えば、配列でもスタック領域に作れる(その代わりあまり大きな数は扱えない)
他の人にはどうでもいいであろう経緯
最初におかしいと思ったのは、「Windowsの場合、プロセスに割り当てられるスタックメモリのサイズは通常1MB程度」という説明をネットで見たときでした。
スタックのメモリを使い果たすと、スタックオーバーフロー(Stack Overflow)のエラーになります。実際プログラミング中に何回か見ましたが、大抵は凡ミスで無限ループに入ってる場合などに限られていました。
えっ?今まで結構フィールドがいっぱいある構造体の配列とか普通に宣言してたけど、1MB程度の制約の中でそんな危ない橋を渡ってたの?
そんなはずはない。何かおかしいな・・・と思ったわけです。
しかしわかってみれば何のことはなく、値型や構造体でも、配列に入れてしまえば配列自体がヒープ領域に置かれているので、値型や構造体であってもヒープ領域に置かれるというだけの話でした。
そもそも「スタック領域」がその名の通り「スタック方式な領域」であるということを理解していればこうした誤解には至らなかったかもしれませんが、論理思考に乏しかったためか、そういう誤解を抱えたままでした。(スタック:LIFO(Last in First out / 最後に入れたものを最初に出す)というデータのやりくり方式)
勝手に誤解してたというだけの話なのですが、同じ勘違いをしてる人がいるかもしれないので・・・。