連載:C# 2.0入門

第6回 部分クラスと静的クラス

株式会社ピーデー 川俣 晶
2007/10/30
Page1 Page2 Page3 Page4

値型と参照型の相違は何か?

 前回のおさらいも兼ねて、話のまくらに匿名メソッドが変数をキャプチャすることの問題を考えてみよう。

 その前に、1つクイズを出そう。

 C#(あるいは.NET Framework)に存在する型は、主に値型と参照型に分類できる。例えば、int型は値型だが、string型は参照型である。自分で型を定義する場合は、structキーワードを使うと値型になり、classキーワードを使うと参照型になる。

 では、値型と参照型はいったい何が違っているのだろうか?

 答えを見る前に少し考えてみよう。

 ……。

 最も基本的な相違は、値型は「値」そのものを受け渡すのに対して、参照型は「参照」を受け渡す点だろう。例えば、メソッドの引数にint型の値を渡すと値の複製が作られてメソッドに渡される(refやoutを付けると参照で渡すこともできるが基本は値)。一方、string型の値を渡すと、値の複製を作るのではなく、実体(オブジェクト)への参照がメソッドに渡される。

 では、ほかに相違はないのだろうか?

 継承やコンストラクタなどの相違もあるが、実際に使ううえで重要な意味を持つのは、やはり寿命の違いだろう。例えば、メソッド内のローカル変数として記述した、値型の変数に入れた値は、スコープを抜けた時点で即座に消えてなくなる。しかし、参照型の変数に入れた値は参照であって実体ではない。そのため、参照が消えても即座に実体が消えるわけではない。それが消えるのは、ほかの参照もすべて消滅したうえで、ガベージ・コレクションが実行された後である。

 これでめでたしめでたし、値型と参照型の相違が明らかになった……というのはC# 1.x時代の話である。

 実は、C# 2.0では、それほど単純な話にはなっていない。

 匿名メソッドによってキャプチャされた変数が消えてなくなるのは、たとえ値型であっても、参照もすべて消滅したうえで、ガベージ・コレクションが実行された後になる。つまり、値型の変数の寿命が参照型と同等になってしまうのである。

 それがもたらすインパクトがピンとこない読者もいると思うので、簡単なサンプル・コードを用意した。

using System;
using System.Diagnostics;

delegate void MyMethodInvoker();

struct TargetStruct
{
  public int[] HugeArray;
}

class Program
{
  static MyMethodInvoker GetMethod()
  {
    TargetStruct t;
    t.HugeArray = new int[100000000];
    return delegate()
    {
      // ↓の行をコメントアウトして比較
      Console.WriteLine(t.HugeArray.Length);
    };
  }

  static void Main(string[] args)
  {
    MyMethodInvoker doit = GetMethod();
    doit();
    GC.Collect();
    Console.WriteLine(
        Process.GetCurrentProcess().VirtualMemorySize64);
  }
}
リスト1 匿名メソッド利用時の値型変数の寿命を見る

 このサンプル・コードの「Console.WriteLine(t.HugeArray.Length);」という行をコメントアウトすると(=匿名メソッドで変数をキャプチャしないと)、プロセスに割り当てられた仮想メモリの量(VirtualMemorySize64プロパティで取得される)は185,020,416bytesとなった(Visual Studio 2005、Debugビルド、Windows Vista Ultimateの環境にて)。

 一方、その行をコメントアウトしないで実行すると、プロセスに割り当てられた仮想メモリの量は587,939,840bytesとなった。

 つまり、劇的にプロセスが保持するメモリ量が異なっているのである。

 なぜその差が生じるのかといえば、変数doitが生きている限り、それによって参照される匿名メソッドは生きていて、そこから参照される変数tもまた生き続け、そこから参照される巨大配列HugeArrayもまた生きているからである。その状況で、「GC.Collect();」を呼び出してガベージ・コレクションさせても、参照が生きているので回収されることはない。

 一方、変数tをキャプチャしない場合は、この変数の寿命はGetMethodメソッドを抜けた時点で尽きてしまう。だから、GC.Collectメソッドを呼び出した時点ではすでに参照が存在しないので、そのためのメモリが残っていても回収されてしまうのである。

 このように、値型の変数をキャプチャすると、匿名メソッドへの参照が残る限りその変数の寿命も尽きず、解放されたと思ったメモリが解放されない問題も起こり得る。単純なサンプル・コードではなかなか起こらないかもしれないが、込み入った実用プログラムのソース・コードでは気付かないうちに起こり得るので、注意が必要だろう。

 余談だが、このようなソース・コードの表面からは分かりにくい挙動を多数持っているのがC#という言語の特徴といえるだろう。この特徴は、よりシンプルな言語の信奉者から批判される点だろう。しかし、シンプルで分かりやすい言語の方がコードを書きやすいか、あるいは読みやすいか……というと、そうともいえない。言語の分かりやすさと、その言語で書かれたソース・コードの書きやすさ、分かりやすさは必ずしも比例しないのである。

 たとえ、表面的に分かりにくい挙動があったとしても、C#の方がより良い結果を出すことも多い。これを解釈するには、よい機能を集めればよい言語が出来上がるという論理が、実は必ずしも成立しないことを学ばねばならない。これは、「合成の誤謬(ごびゅう)」と呼ばれる問題の一種といえる。言語のデザインとは、単純な理屈で割り切れない世界なのである。


 INDEX
  C# 2.0入門
  第6回 部分クラスと静的クラス
  1.値型と参照型の相違は何か?
    2.部分クラス/自動生成コードと安全に共存する
    3.リフレクションと部分クラス/部分クラスの注意点
    4.静的クラス/アクセサのアクセシビリティ/アクセシビリティ指定の制約
 
インデックス・ページヘ  「C# 2.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間