公式ページでの解説
※この記事は2020年12月に書いています。
.NETの開発者向けの公式ドキュメントの記事はこちら
一部引用すると
ガベージ コレクターによってクラス インスタンスが収集されている場合は、ファイナライザー (デストラクターとも呼ばれます) を使用して、最終的に必要なすべてのクリーンアップが実行されます。
とありますが、そうでもないようです。
サンプルコードの実行結果
上記ページにサンプルコードがあります。
using System.Diagnostics;
public class ExampleClass
{
Stopwatch sw;
public ExampleClass()
{
sw = Stopwatch.StartNew();
Console.WriteLine("Instantiated object");
}
public void ShowDuration()
{
Console.WriteLine("This instance of {0} has been in existence for {1}",this, sw.Elapsed);
}
~ExampleClass()
{
Console.WriteLine("Finalizing object");
sw.Stop();
Console.WriteLine("This instance of {0} has been in existence for {1}",this, sw.Elapsed);
}
}
public class Demo
{
public static void Main()
{
ExampleClass ex = new ExampleClass();
ex.ShowDuration();
}
}
これを.NET Framework 上で実行すると記事にあるとおり以下のような結果になります。
This instance of ExampleClass has been in existence for 00:00:00.0008002
Finalizing object
This instance of ExampleClass has been in existence for 00:00:00.0020109
同じコードを.NET 5 上で実行すると以下のような結果になります。
This instance of ExampleClass has been in existence for 00:00:00.0144380
つまり、ファイナライザーが実行されていません。
GitHub上での議論
検索すると上記のようにファイナライザーに関する議論がいくつかみつかります。
そこではファイナライザーの代替としてアセンブリのアンロードイベントの使用について述べられています。
using System.Reflection;
using System.Runtime.Loader;
namespace ConsoleApp1103
{
class Program
{
static void Main(string[] args)
{
var tester = new Tester();
var current = Assembly.GetExecutingAssembly();
AssemblyLoadContext.GetLoadContext(current).Unloading += sender =>
{
Console.WriteLine("コンテクストアンロード");
};
GC.Collect();
tester = null;
GC.Collect();
GC.Collect();
}
}
public class Tester
{
public Tester() => Console.WriteLine("コンストラクタ");
~Tester() => Console.WriteLine("ファイナライザー");
}
}
このコードを実行すると
コンテクストアンロード
となります。
もう少し詳しく
プロセス実行中に.NETによるメモリ開放処理は.NET Frameworkと同様に.NET Coreや.NET 5でも実行されます。
プロセス終了時に未実行のファイナライザーは.NET Frameworkでは呼ばれますが、.NET Coreや.NET 5では呼ばれません。
WPFの画面にボタンを一つ張り付けたもののコードビハインドです。
using System.Reflection;
using System.Windows;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Debug.WriteLine("スタート");
var current = Assembly.GetExecutingAssembly();
AssemblyLoadContext.GetLoadContext(current).Unloading += sender =>
{
Debug.WriteLine("アプリケーション終了");
};
}
private int count = 0;
private void Button_Click(object sender, RoutedEventArgs e)
{
var tester = new Tester(++count);
tester = null;
}
public class Tester
{
private int _id;
public Tester(int id)
{
_id = id;
Debug.WriteLine($"☆コンストラクタ {_id}");
}
~Tester() => Debug.WriteLine($"★ファイナライザー {_id}");
}
}
}
※上記は.NET 5 のコードです。.NET FrameworkではAssemblyLoadContextがないのでApplication.Exitイベントで代替して確認しました。
上のコードはボタンを押下する毎にTesterクラスを生成し、そのインスタンスへの参照を切ることによりGC可能とします。
.NET Frameworkで実行すると
☆コンストラクタ 1
☆コンストラクタ 2
☆コンストラクタ 3
☆コンストラクタ 4
☆コンストラクタ 5
★ファイナライザー 5
★ファイナライザー 4
★ファイナライザー 3
★ファイナライザー 2
★ファイナライザー 1
☆コンストラクタ 6
☆コンストラクタ 7
☆コンストラクタ 8
アプリケーション終了
★ファイナライザー 8
★ファイナライザー 7
★ファイナライザー 6
のようなコードが出力されます。8回押下してインスタンスを8つ作る途中で一度ファイナライザーが実行され(される場合とされない場合があります。実行契機は完全に.NET Frameworkお任せです)、×ボタンで画面終了させる後に未実行のファイナライザーが3つ実行されています。
これを.NET 5 で実行すると
☆コンストラクタ 1
☆コンストラクタ 2
☆コンストラクタ 3
★ファイナライザー 3
★ファイナライザー 2
★ファイナライザー 1
☆コンストラクタ 4
☆コンストラクタ 5
☆コンストラクタ 6
☆コンストラクタ 7
☆コンストラクタ 8
アプリケーション終了
ファイナライザーの実行契機はお任せですが、終了時に未実行分が実行されていません。