C#でリフレクションを使って強制的にFinalizeを呼び出してオブジェクトを削除するとどうなるか

こんにちは、Takymです。
今回はちょっと変わった検証をしたいと思います。
強制的にデストラクタを呼び出してみるとどうなるかの実験です。
何故、この検証をしようと思ったのかと言うと、IDisposableを実装していないクラスでも、手動で解放させたかったからです。

どうやって強制的に呼び出すか

まず.NETには、リフレクションと言う機能が用意されていて、動的にクラスのインスタンスを作成したり、
クラスのメンバにアクセス(privateも)できたりします。
リフレクションは以下の様な感じで行います。

using System;
using System.Reflection;
namespace ForTest
{
class Program
{
private string value = "0123456789";
public override string ToString()
{
return "valueフィールドの値:" + value.ToString();
}
static void Main(string[] args)
{
// オブジェクト作成
object p = new Program();
// オブジェクトの型情報を取得
Type t = p.GetType();
// valueフィールドの情報を取得
FieldInfo fi = t.GetField(
"value",                  // メンバの名前
BindingFlags.NonPublic |  // publicでないメンバも検索に含める
BindingFlags.GetField |   // 読み取り可能のフィールド
BindingFlags.SetField |   // 書き込み可能のフィールド
BindingFlags.Instance);   // インスタンス メンバ
// valueフィールドの値を表示
Console.WriteLine(fi.GetValue(p));
// valueフィールドの値を書き換え
fi.SetValue(p, "Rewrited");
// ToStringでvalueフィールドの値を表示
Console.WriteLine(p.ToString());
// 何かキーを押されるまで待機
Console.ReadKey(true);
}
}
}

上記のプログラムを実行するとコンソール画面には以下のように表示されます。

0123456789
valueフィールドの値:Rewrited

これを利用して、Finalizeメソッド(デストラクタ)を呼び出そうという事です。

さっそく実験!

まずは、以下のコードを見てください。

using System;
using System.Reflection;
namespace Test
{
/// <summary>
///  このアプリケーションのメインクラスです。
/// </summary>
public static class Program
{
/// <summary>
///  このアプリケーションのエントリポイントです。
/// </summary>
/// <param name="args">コマンドライン引数を取得します。</param>
/// <returns>OSに実行結果を表す数値を返します。</returns>
[STAThread()]
public static int Main(params string[] args)
{
// 普通のオブジェクトを削除する
object a = new object();
Console.WriteLine("Delete前(a):" + a);
Delete(a);
GC.Collect(); // これを呼び出してメモリから完全に削除する
Console.WriteLine("Delete後(a):" + a);
// 簡易的なオブジェクトを削除する
Data b = new Data("Hello, World!!");
Console.WriteLine("Delete前(b.Text):" + b.Text);
Delete(b);
GC.Collect(); // これを呼び出してメモリから完全に削除する
Console.WriteLine("Delete後(b.Text):" + b.Text);
// バイト配列を削除する
byte[] c = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine("Delete前(c):" + string.Join(",", c));
Delete(c);
GC.Collect(); // これを呼び出してメモリから完全に削除する
Console.WriteLine("Delete後(c):" + string.Join(",", c));
// バイト配列を保持したオブジェクトを削除する
Data2 d = new Data2(9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
Console.WriteLine("Delete前(d.Values):" + string.Join(",", d.Values));
Delete(d);
GC.Collect(); // これを呼び出してメモリから完全に削除する
Console.WriteLine("Delete後(d.Values):" + string.Join(",", d.Values));
Console.ReadKey(true);
return 0;
}
/// <summary>
///  指定したオブジェクトのデストラクタを呼び出して削除します。
/// </summary>
/// <param name="obj">削除するオブジェクトです。</param>
public static void Delete(object obj)
{
var mi = obj.GetType().GetMethod(
"Finalize",
BindingFlags.NonPublic |
BindingFlags.InvokeMethod |
BindingFlags.Instance);
mi.Invoke(obj, null);
GC.SuppressFinalize(obj);
}
/// <summary>
///  文字列データを保持するクラスです。
/// </summary>
public class Data
{
/// <summary>
///  文字列データです。
/// </summary>
public string Text { get; set; }
/// <summary>
///  型'<see cref="Test.Program.Data"/>'の新しいインスタンスを生成します。
/// </summary>
/// <param name="str">このクラスに保存する文字列です。</param>
public Data(string str) {
this.Text = str;
}
/// <summary>
///  現在のオブジェクトのリソースを解放します。
/// </summary>
~Data() {
this.Text = null;
Console.WriteLine("デストラクタが呼び出されました。");
}
}
/// <summary>
///  バイト配列を保持するクラスです。
/// </summary>
public class Data2
{
/// <summary>
///  バイト配列です。
/// </summary>
public byte[] Values { get; set; }
/// <summary>
///  型'<see cref="Test.Program.Data2"/>'の新しいインスタンスを生成します。
/// </summary>
/// <param name="values">このクラスに保存するバイト配列です。</param>
public Data2(params byte[] values) {
this.Values = values;
}
}
}
}

実行するとどのようになると思いますか?
僕は、最初、実行時エラーが発生すると思っていました。
しかし実行したらコンソールに以下の様に表示されました。

Delete前(a):System.Object
Delete後(a):System.Object
Delete前(b.Text):Hello, World!!
デストラクタが呼び出されました。
Delete後(b.Text):
Delete前(c):0,1,2,3,4,5,6,7,8,9
Delete後(c):0,1,2,3,4,5,6,7,8,9
Delete前(d.Values):9,8,7,6,5,4,3,2,1,0
Delete後(d.Values):9,8,7,6,5,4,3,2,1,0

Data1以外のクラスは破棄されていないように見えるのです。

「teratail」で質問してみた。

この結果は少し不安になったので、teratail.comと言う質問箱サービスで質問してみました。

すると以下の様な答えが返ってきました。

(中略)
これはリフレクションを使用するしないの違いはないのではないかと思います。
(中略)
Finalize後にそのインスタンスを使用するのはやってはいけないですよね。
要はリフレクションによるFinalizeの呼び出しが問題なのではなく、Finalize後にそのインスタンスを利用するのが問題かと思います。

この回答を見た時、Finalizeメソッド呼び出し後にオブジェクトにアクセスするという事自体が、
間違っているという事を始めて気づきました

感想

リフレクションという事自体、推奨されていないので今後は利用に気を付ける事にしました。
感想・意見・誤字・脱字等はこの記事のコメント欄にお願いします。
最後まで読んでくださってありがとうございました。

コメントを残す

WordPress.com で次のようなサイトをデザイン
始めてみよう