オブジェクトの破棄

ガベージコレクション

プログラミングではあらゆるデータをメモリ上に保存し利用します。
メモリは有限なので、不要になったデータは破棄してメモリを解放する必要があります。

C#(.NET Framework)ではガベージコレクションという機能があり、不要になったデータは自動的に解放されます。
(Garbage Collection=ゴミ収集)
どのデータが不要か、というのはどこかから参照されているか否かで判断されます。


int[] arr = new int[100];
for (int i = 0; i < arr.Length; i++)
    arr[i] = i;

//参照を切る
arr = null;

配列の場合も当然ながらメモリ上に実データがあります。
この実データは配列型変数arrから参照されている状態です。

変数arrにnullを代入すると、メモリ上のどこも指していない状態になります。
今まで指していた配列の実データにアクセスする手段がなくなり、不要なデータとなります。

こういったデータはC#が自動的に破棄します。

ガベージコレクションはデータが不要になった瞬間にメモリを解放するのではなく、適切なタイミングでC#が自動的に解放してくれます。
プログラマーはその処理を意識する必要はありません。

明示的に動作させることも可能ですが、メモリの解放というのは結構重たい処理なのでC#に任せてしまった方がほとんどの場合で高速です。
明示的な解放はテストなどの特殊な場合を除き使用しません。

当然ですが、配列型変数にnullを代入しても別の場所から参照されていればガベージコレクションが働いてもメモリは解放されません。


int[] arr1 = new int[100];
for (int i = 0; i < arr1.Length; i++)
    arr1[i] = i;

int[] arr2 = arr1;

arr1 = null;

//arr1が参照していた領域は
//arr2が参照しているので
//メモリは解放されない

コードが複雑になると、思わぬところからの参照状態が続いていてメモリがなかなか解放されない、ということが起こり得ます。
できる限り「不要になったら参照を切る」ことを心がけましょう。

IDisposable

.NET Framework標準ライブラリのクラスには、内部的に多くのメモリを確保するものがあります。
これらも不要になったら自動的に解放されるのですが、不要になったことを明示的にガベージコレクションに知らせるためのメソッドが用意されているものがあります。
典型的な例としてはStreamクラスです。


var fs = new System.IO.FileStream(
    "test.txt",
    System.IO.FileMode.Open);

//fsを利用して何か処理...

//ストリームを閉じる
fs.Close();

//↓でもよい
//fs.Dispose();

Streamクラスの場合、保持しているメモリ領域を解放するにはCloseメソッドかDisposeメソッドを呼び出します。
Closeメソッドは内部的にDisposeメソッドを呼び出しているだけですので、どちらを使用しても構いません。

こういったクラスはIDisposableというインターフェイス継承しています。
IDisposableインターフェイスにはDisposeメソッドが定義されており、これを呼び出してメモリを破棄します。

Disposeメソッドの実装

自作クラスでメモリ解放の処理を実装する場合はIDisposableインターフェイスを継承し、Disposeメソッドを実装します。


//IDisposableを継承
class SimpleClass : IDisposable
{
    FileStream fstream;

    public SimpleClass(string path)
    {
        fstream = new FileStream(path, FileMode.Open);
    }

    //Disposeメソッドを実装
    public void Dispose()
    {
        fstream.Close();
    }
}

SimpleClass sc = new SimpleClass("test.txt");

//何か処理...

sc.Dispose();

IDisposableインターフェイスを継承せずとも、Disposeメソッドと同じ動作をするメソッドを定義すればメモリの解放は可能です。
しかしメモリ解放をするメソッドは上記の形で実装することが推奨されます。
それは次に説明するusingステートメントが利用できるためです。

usingステートメント

メモリの解放は明示的に行いますが、ローカル変数として使用する場合はusingステートメントを使用することが推奨されます。


using (var fs = new System.IO.FileStream(
    "test.txt",
    System.IO.FileMode.Open))
{

    //fsを利用して何か処理...

}//「fs」の寿命はここまで

「using」に続く括弧内でIDisposableを継承したクラスのインスタンス生成します。
続くブロック内で先ほどのインスタンスを利用した処理を書きます。

この方法で生成したインスタンスは、usingブロックを抜けると自動的にDisposeメソッドが呼び出されます。
Disposeメソッドを明示的に呼び出す必要がないので、メモリの解放のし忘れがなくなります。

自作クラスでもIDisposableを継承させればusingステートメントを利用できます。

複数のusingステートメント

IDisposableなインスタンスを複数同時に使用する場合は以下のようにusingステートメントを連続して記述できます。


using (var fs = new System.IO.FileStream(/*省略*/))
using (var ms = new System.IO.MemoryStream())
{
    //省略
}

usingブロック内ではfsとmsが同時に利用可能です。
これは以下のように入れ子にしたのと同じ意味になります。


using (var fs = new System.IO.FileStream(/*省略*/))
{
    using (var ms = new System.IO.MemoryStream())
    {
        //省略
    }
}

usingステートメントとtry~finally文

usingステートメントはコンパイル時に例外try~finally文に自動的に置き換えられます。


using (var fs = new System.IO.FileStream(/*省略*/))
{
    //省略
}

//↓以下のコードに置き換え

var fs = new System.IO.FileStream(/*省略*/);
try
{
    //省略
}
finally
{
    if (sr != null)
        sr.Dispose();
}

finally文はtry文で例外が発生しても実行されるので、確実にDisposeが呼び出されるわけです。

管理リソースと非管理リソース

Streamクラスなどは.NET Frameworkで提供される機能で、リソースの確保や破棄などの制御はすべて.NET Frameworkが行います。
このようなリソースを管理リソース(マネージリソース)といいます。

対して.NET Frameworkが関与しないものを非管理リソース(アンマネージリソース)といいます。
例えばC#からWindows APIなどを直接呼び出して使用する場合です。
.NET Frameworkはいろいろな機能が提供されていますが、OSが提供するすべての機能がサポートされているわけではないので、そういった部分を触りたい場合は非管理リソースを使用する場合もあります。

管理リソースは最悪メモリの解放処理を忘れても、いつかはガベージコレクションがメモリを解放してくれます。
(ただし必要もないのに残っているのは無駄なので、必要がなくなった時点で適切に解放処理はすべきです)
しかし非管理リソースは.NET Frameworkの管理外のことなので、最悪はメモリリークによりプログラムがクラッシュします。
(メモリリーク=メモリが解放されずに残り続けてメモリ不足になること)

管理リソースだけを使用する場合は、上のサンプルコードで示したようにIDisposableインターフェイスを継承してDisposeメソッドを実装するだけで問題ありません。
もし非管理リソースを使用する場合は、オブジェクトの破棄処理を少し変更する必要があります。


public class SimpleClass : IDisposable
{
    //二重解放を避けるためのフラグ
    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                //管理リソースの破棄処理をここに記述
            }

            //非管理リソースの破棄処理をここに記述

            disposedValue = true;
        }
    }
    
    // ファイナライザー
    ~SimpleClass()
    {
        Dispose(false);
    }
    
    public void Dispose()
    {
        Dispose(true);

        //ファイナライザーを呼ばない事を
        //ガベージコレクションに指示する
        GC.SuppressFinalize(this);
    }
}

インスタンスの初期化時にコンストラクターがあるように、インスタンスの破棄時にはファイナライザーという処理を書くことができます。
ファイナライザーはクラス名の前に「~」(チルダ)を付けた特殊なメソッド名で記述します。

通常の手順通りDisposeメソッドを呼んでオブジェクトを破棄する場合、GC.SuppressFinalizeメソッドでガベージコレクションに対してファイナライザーを実行しないように指示します。
すでに解放されたリソースをさらに解放する必要はないからです。

Disposeメソッドの呼び出しを忘れてしまった場合、インスタンスの破棄時にファイナライザーが呼ばれます。
ファイナライザー呼び出しにより非管理リソースは解放されます。
管理リソースの解放はガベージコレクションに任せます。

ファイナライザーによるリソースの解放はあくまでも保険で、できるだけDisposeメソッドを呼び出して解放すべきです。