C#などのオブジェクト指向のプログラミング言語で開発を行っていると、ガベージコレクションが働くのでメモリを意識しなくてもプログラミングができちゃいます。(ガベージコレクション対象外の領域もありますけど)
文系プログラマーの代表的な存在である私は、特に学校でメモリについて学んできたわけではありません。開発初心者の頃は、とにかく仕様書通りの開発を行うだけで精いっぱいで、メモリなんて全く意識していませんでした。(笑)
ですが、開発経験が増えてくると、メモリについての理解は必要不可欠だと思えるようになってくるんですよね。特にヒープとスタック領域についての理解はあった方がいいです。
IT業界では、最近は文系出身の開発者がめちゃくちゃ増えてきました。文系出身のプログラマーの質を向上させないと、世の中に出回るアプリケーションの質も向上しないと常日頃から思っています(大袈裟かよ)ので、ここでは文系出身のプログラマー必見のメモリについてのあれこれをまとめてみました。参考になれば幸いです。
目次
値型と参照型について
C#を学習していると、初心者の方がまず躓くところがあります。それが値型と参照型の違いではないでしょうか。ちょっとしたプログラムをそれぞれのパターンで作ってみたので、まずはそちらを確認してみましょう。
値型
値型で代表されるのが、構造体です。構造体の他にはint型やdecimal型やenum(列挙)型などが当てはまるところです。この値型は実際の値(データ)を変数に格納します。以下のイメージを参考にしてみてください。スタック領域(後で詳しくは説明)に値を格納していますね。
以下、構造体(値型)のサンプルプログラムです。structTest1.NameTestとstructTest2.NameTestに格納される値を確認してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System; using System.Windows.Forms; namespace memory_test { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { StructTest structTest1 = new StructTest(0,@"Nagoya"); Console.WriteLine(structTest1.NameTest); structTest1.NameTest = @"Osaka"; StructTest structTest2 = structTest1; structTest2.NameTest = @"Tokyo"; Console.WriteLine(structTest1.NameTest); Console.WriteLine(structTest2.NameTest); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace memory_test { struct StructTest { public int IntTest { get; set; } public string NameTest { get; set; } //デフォルトコンストラクタを明示的に呼ぶ方法 public StructTest(int id, string name) : this() { IntTest = id; NameTest = name; } } } |
実行結果
上記のサンプルプログラムの結果を見てみると、structTest1.NameTestとstructTest2.NameTestの値はそれぞれ違っています。structTest1からstructTest2に代入していますが、これは実はstructTest1のコピー版を渡しているのですね。ですので、structTest1.NameTestはそのまま("Osaka")で、structTest2.NameTestには新しい値("Tokyo")が格納されることになるのです。
参照型
参照型で代表されるのが、クラスです。クラスの他にはstring型や配列が当てはまりますね。メモリ(1バイト=8ビット毎)にはそれぞれアドレス番号(番地)が16進数で振り分けられています。参照型ではこのアドレス番号(番地)を参照しています。実際の値はヒープ領域(後で詳しくは説明)に格納されています。
以下、クラス(参照型)のサンプルプログラムです。classtest1.NameTestとclasstest2.NameTestに格納される値を確認してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using System; using System.Windows.Forms; namespace memory_test { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { using (ClassTest classtest1 = new ClassTest(0, @"Nagoya")) { Console.WriteLine(classtest1.NameTest); classtest1.NameTest = @"Osaka"; ClassTest classtest2 = classtest1; classtest2.NameTest = @"Tokyo"; Console.WriteLine(classtest1.NameTest); Console.WriteLine(classtest2.NameTest); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
using System; namespace memory_test { class ClassTest : IDisposable { public int IntTest { get; set; } public string NameTest { get; set; } public ClassTest(int id, string name) { IntTest = id; NameTest = name; } #region IDisposable Support private bool disposedValue = false; // 重複する呼び出しを検出するには protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: マネージド状態を破棄します (マネージド オブジェクト)。 } // TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、下のファイナライザーをオーバーライドします。 // TODO: 大きなフィールドを null に設定します。 disposedValue = true; } } // TODO: 上の Dispose(bool disposing) にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします。 // ~ClassTest() { // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。 // Dispose(false); // } // このコードは、破棄可能なパターンを正しく実装できるように追加されました。 void IDisposable.Dispose() { // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。 Dispose(true); // TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。 // GC.SuppressFinalize(this); } #endregion } } |
実行結果
上記のサンプルプログラムの結果を見てみると、classtest1.NameTestとclasstest2.NameTestの値が同じ("Tokyo")です。実際のデータはヒープ領域に存在しています。ここではそれぞれの変数にアドレス番号(番地)を格納しています。同じアドレス番号(番地)を参照しているから同じ値になっているのですね。
ヒープ領域とスタック領域について
ヒープ領域とは、プログラムの実行時に動的に確保されるメモリの領域です。このヒープ領域は、領域を確保すると破棄されるまでずっとメモリの領域を掴み続けます。
私がよく利用するC#では、クラスのインスタンス化を行うためにnew演算子を活用します。これは言わばヒープ領域から動的メモリを割り当てしていることになります。
ヒープ領域には、インスタンスや実際の値、そして静的領域(static)などが格納されています。静的領域とは、インスタンス単位で生成するのではなく、アプリケーション単位で1つだけ生成したい時に利用します。共通関数とかはこれに該当しますかね。
スタック領域とは、主にプログラムの実行を制御する領域です。メソッドなどが格納されます。いわゆるローカル変数は、すべてスタック領域に格納されていることになります。スタック領域に格納された処理は、上から順番に実行されます。実行済みの処理は破棄されていきます。
自動的にインスタンスを破棄してくれるガベージコレクションは、ヒープ領域のインスタンスに対して働きます。但し、データベースなどの外部リソースなどはガベージコレクション対象外です。プログラマーが明示的にインスタンスを破棄する場合は、C#ですとDisposeメソッドやusingステートメントを利用するのが一般的です。
以下、簡単(超大雑把)なイメージです。
まとめ
一流のエンジニアたるものメモリについての理解は必要不可欠です。ユーザーが要求する仕様書通りにシステムを作るのはもちろん、ユーザーからは直接目に触れることがないサーバー側での処理は、動けばいいや的な感じでコーディングされがち(妄想)なんで、そこは手を抜かずメモリを超絶意識してコーディングしていきましょう。
最後までお読みいただきありがとうございました。