C#のデストラクタとは?Finalize・IDisposableとの違いと使い方を初心者向けに解説
はじめに
C#の「デストラクタ」は、オブジェクトが不要になったあとに最後の後始末を行うための仕組みです。ただし、C++などのデストラクタと同じ感覚で使うと、思ったタイミングで呼ばれなかったり、パフォーマンスを悪化させたりすることがあります。
現在のC#では、デストラクタは「ファイナライザー」と呼ばれることが多く、Finalize、IDisposable、Dispose、usingとセットで理解することが重要です。Microsoftの公式ドキュメントでも、C#のファイナライザーは以前「デストラクター」と呼ばれていたもので、ガベージコレクターによる回収時に最終的なクリーンアップを行う仕組みだと説明されています。
この記事では、「C# デストラクタ」と検索している初心者の方に向けて、デストラクタの基本、Finalizeとの関係、IDisposableとの違い、実務での正しい判断基準まで順番に解説します。
1. C#のデストラクタとは?初心者向けに基本を解説
1-1. C#におけるデストラクタの役割
C#のデストラクタは、クラスのインスタンスがガベージコレクションによって回収されるときに呼び出される可能性がある特別なメソッドです。主な役割は、通常のメモリ解放ではなく、アンマネージリソースの最終的な後始末です。
C#では、通常のオブジェクトのメモリ管理はガベージコレクターが自動で行います。そのため、単に「使い終わったオブジェクトのメモリを解放したい」という理由でデストラクタを書く必要はありません。
たとえば、次のような通常のクラスではデストラクタは不要です。
C#public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
このクラスは文字列や数値など、.NETが管理する通常のデータしか持っていません。こうしたマネージオブジェクトはGCが管理するため、開発者がデストラクタで解放処理を書く必要はありません。
1-2. デストラクタの基本構文:~クラス名()
C#のデストラクタは、クラス名の前にチルダ~を付けて書きます。
C#class Sample
{
~Sample()
{
// 後始末の処理
}
}
構文上の特徴は次のとおりです。
C#class クラス名
{
~クラス名()
{
// クリーンアップ処理
}
}
C#のファイナライザーは、クラスにだけ定義でき、構造体には定義できません。また、1つのクラスに定義できるファイナライザーは1つだけで、修飾子や引数を指定することもできません。さらに、自分で直接呼び出すことはできず、実行タイミングはランタイム側に任されます。
そのため、次のような書き方はできません。
C#public ~Sample() // public は付けられない
{
}
~Sample(int value) // 引数は指定できない
{
}
デストラクタは一見するとメソッドのように見えますが、通常のメソッドとは扱いが大きく異なります。
1-3. デストラクタはいつ呼ばれるのか
C#のデストラクタは、オブジェクトがGCの対象になり、ガベージコレクターが必要だと判断したときに呼び出されます。
重要なのは、「変数がスコープを抜けた瞬間に呼ばれるわけではない」という点です。
C#void Method()
{
var sample = new Sample();
} // ここで必ずデストラクタが呼ばれるわけではない
上記のsampleはメソッドを抜けると参照されなくなる可能性がありますが、その時点で即座にデストラクタが実行されるとは限りません。GCがいつ実行されるかは、メモリ状況やランタイムの判断に左右されます。
公式ドキュメントでも、ファイナライザーがいつ呼び出されるかをプログラマーは制御できず、ガベージコレクターによって決定されると説明されています。
1-4. C++のデストラクタとの違い
C++のデストラクタは、オブジェクトの寿命が終わったタイミングで比較的明確に呼び出されます。たとえば、スタック上のオブジェクトであればスコープを抜けたときにデストラクタが実行されます。
一方、C#のデストラクタはGCに管理されているため、呼び出しタイミングが確定していません。
違いを簡単にまとめると、次のようになります。
| 項目 | C#のデストラクタ | C++のデストラクタ |
|---|---|---|
| 呼び出しタイミング | GCが判断する | オブジェクト寿命に応じて比較的明確 |
| 明示的な呼び出し | できない | 通常は自動だが設計上明確 |
| 主な用途 | アンマネージリソースの最終処理 | リソース解放全般 |
| 実務での使用頻度 | 低い | 高い |
C#では、リソースを確実に解放したい場合、デストラクタよりもIDisposableとusingを使うのが基本です。
2. C#のデストラクタが必要になるケース
2-1. アンマネージリソースとは何か
アンマネージリソースとは、.NETのガベージコレクターが直接管理できない外部リソースのことです。
代表例としては、次のようなものがあります。
| リソース | 例 |
|---|---|
| OSハンドル | ウィンドウハンドル、ファイルハンドル |
| ネイティブメモリ | Marshal.AllocHGlobalで確保したメモリ |
| ネイティブライブラリ | C/C++のDLLが返すポインタ |
| 外部接続 | ソケット、特定の低レベル接続 |
GCはマネージオブジェクトのメモリ解放は行えますが、ウィンドウハンドル、開いているファイル、ストリームなどのアンマネージリソースについては直接把握できません。IDisposableの主な用途も、こうしたアンマネージリソースを明示的に解放することです。
2-2. ファイル・ハンドル・DB接続などのリソース管理
初心者が混乱しやすいのが、ファイルやDB接続を使うときに「デストラクタを書くべきか」という点です。
結論から言うと、多くの場合は自分でデストラクタを書く必要はありません。
たとえば、FileStream、StreamReader、SqlConnectionなどは、内部でリソース管理の仕組みを持っており、利用者はDisposeやusingで解放します。
C#using var reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);
この場合、StreamReaderの利用者であるあなたがデストラクタを書くのではなく、usingによってDisposeを確実に呼び出すのが正しい使い方です。usingステートメントは、ブロックを抜けるときにIDisposableインスタンスが破棄されるようにする構文です。例外が発生した場合でも破棄されるように設計されています。
2-3. 通常のクラスではデストラクタが不要な理由
通常のクラスでは、次のような理由からデストラクタは不要です。
1つ目は、メモリ解放はGCが行うためです。C#では、使われなくなったマネージオブジェクトのメモリはGCによって回収されます。
2つ目は、デストラクタを書くとGCの処理が重くなる可能性があるためです。ファイナライザーを持つオブジェクトは、通常のオブジェクトよりも回収に手間がかかります。公式ドキュメントでも、空のファイナライザーや不要なファイナライザーはパフォーマンスを不必要に低下させる原因になると説明されています。
3つ目は、呼び出しタイミングが不確定なためです。確実にファイルを閉じたい、DB接続を返したい、ロックを解放したいといった処理には向いていません。
2-4. デストラクタを使うべきケース・使わないべきケース
デストラクタを使うべきケースはかなり限定的です。
使う可能性があるケースは、クラスがアンマネージリソースを直接所有していて、Disposeが呼ばれなかった場合の最後の保険を用意したい場合です。
C#public class NativeResourceHolder
{
private IntPtr _handle;
~NativeResourceHolder()
{
// Disposeが呼ばれなかった場合の最後の保険
// アンマネージリソースのみを解放する
}
}
一方で、次のような場合はデストラクタを使うべきではありません。
| ケース | 理由 |
|---|---|
| 通常のデータクラス | GCに任せればよい |
| ログ出力したいだけ | 呼び出しタイミングが不確定 |
| マネージオブジェクトを解放したい | ファイナライザー内では安全に扱えない |
| ファイルやDB接続を使うだけ | usingやDisposeを使うべき |
| デバッグ確認したいだけ | 実務コードに残すべきではない |
C#では、「デストラクタを書く」のではなく、「まずIDisposableとusingで設計する」と考えるのが基本です。
3. C#のデストラクタの書き方とサンプルコード
3-1. 最小構成のデストラクタ例
最もシンプルなデストラクタは次のように書きます。
C#using System;
class Sample
{
~Sample()
{
Console.WriteLine("デストラクタが呼ばれました");
}
}
ただし、このコードは学習用の例です。実務でConsole.WriteLineだけを目的にデストラクタを書くことはおすすめできません。
デストラクタは、public、private、protectedなどのアクセス修飾子を付けません。また、戻り値も引数もありません。
C#class Sample
{
~Sample()
{
// クリーンアップ処理
}
}
3-2. デストラクタの動作を確認するサンプル
デストラクタが呼ばれる様子を確認するには、次のようなコードを使えます。
C#using System;
class Sample
{
public Sample()
{
Console.WriteLine("コンストラクタが呼ばれました");
}
~Sample()
{
Console.WriteLine("デストラクタが呼ばれました");
}
}
class Program
{
static void Main()
{
CreateObject();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Main終了");
}
static void CreateObject()
{
var sample = new Sample();
}
}
この例では、CreateObjectメソッドの中でSampleインスタンスを作成しています。メソッドを抜けるとsampleへの参照がなくなるため、GCの対象になる可能性があります。
その後、GC.Collect()とGC.WaitForPendingFinalizers()を呼び出して、学習目的でGCとファイナライザーの実行を待っています。
3-3. GC.Collectを使った確認時の注意点
GC.Collect()を使うと、ガベージコレクションを明示的に要求できます。しかし、通常のアプリケーションコードで頻繁に使うべきではありません。
公式ドキュメントでも、GC.Collectによってガベージコレクションを強制できるものの、パフォーマンスに問題が発生する可能性があるため、通常は避けるべきだと説明されています。
つまり、GC.Collect()は「デストラクタの動作を学習用に確認するため」に使うものであり、実務でリソース解放のために使うものではありません。
悪い例は次のようなコードです。
C#// リソースを解放したいからGCを強制する、という考え方はよくない
GC.Collect();
リソースを確実に解放したいなら、GCを強制するのではなく、Disposeを呼ぶ設計にします。
C#using var resource = new MyResource();
// ここでresourceを使う
3-4. デストラクタ内でやってはいけない処理
デストラクタ内では、何でも自由に処理できるわけではありません。
特に避けるべき処理は次のとおりです。
| やってはいけない処理 | 理由 |
|---|---|
| マネージオブジェクトへのアクセス | すでに無効な状態の可能性がある |
| 例外を投げる処理 | アプリケーションに重大な影響を与える可能性がある |
| 重い処理 | GCや終了処理を遅くする |
| 外部通信 | 成功する保証がなく遅延の原因になる |
| ログ出力前提の設計 | 呼び出しタイミングが保証されない |
公式ドキュメントでも、ファイナライザーからマネージドオブジェクトメンバーにアクセスしてはいけないと説明されています。最終処理中には、マネージドオブジェクトがすでに破棄されていたり、無効な状態になっていたりする可能性があるためです。
デストラクタ内では、原則として「直接所有しているアンマネージリソースの解放」だけを行うと考えましょう。
4. デストラクタとFinalizeの違い
4-1. Finalizeとは何か
Finalizeは、.NETのすべてのオブジェクトの基底クラスであるobjectに関連する終了処理の仕組みです。
C#で書く~クラス名()というデストラクタ構文は、内部的にはFinalizeメソッドとして扱われます。そのため、現在のC#では「デストラクタ」というより「ファイナライザー」と呼ぶほうが正確です。
たとえば、次のコードを考えます。
C#class Sample
{
~Sample()
{
Console.WriteLine("Finalize相当の処理");
}
}
この~Sample()は、C#コンパイラーによってFinalizeに対応する処理へ変換されます。
4-2. デストラクタはFinalizeに変換される仕組み
C#のファイナライザーは、基底クラスのFinalizeを暗黙的に呼び出します。公式ドキュメントでは、ファイナライザー呼び出しは概念的に次のようなコードへ解釈されると説明されています。
C#protected override void Finalize()
{
try
{
// cleanup statements...
}
finally
{
base.Finalize();
}
}
実際にC#でこのコードを直接書くわけではありません。開発者は次のようにデストラクタ構文を書きます。
C#class Sample
{
~Sample()
{
// cleanup statements...
}
}
つまり、C#のデストラクタは「Finalizeを書くための専用構文」と理解するとわかりやすいです。
4-3. Finalizeを直接オーバーライドできない理由
C#では、通常のメソッドのようにFinalizeを直接オーバーライドするのではなく、~クラス名()という構文を使います。
次のようなコードはC#では書きません。
C#class Sample
{
protected override void Finalize()
{
// C#ではこのように直接書かない
}
}
C#では、ファイナライザー構文によって安全にFinalize相当の処理を定義します。これにより、基底クラスのFinalize呼び出しなど、必要な処理がコンパイラー側で扱われます。
4-4. デストラクタとFinalizeの関係をコードで理解する
次のコードは、C#での書き方と内部的なイメージの対応を示しています。
C#で書くコードは次のとおりです。
C#class ResourceHolder
{
~ResourceHolder()
{
Console.WriteLine("クリーンアップ");
}
}
内部的なイメージは次のようになります。
C#protected override void Finalize()
{
try
{
Console.WriteLine("クリーンアップ");
}
finally
{
base.Finalize();
}
}
このように、C#のデストラクタとFinalizeは別物というより、「C#の構文としてはデストラクタ、.NETの仕組みとしてはFinalize」と理解するとよいでしょう。
5. デストラクタとIDisposableの違い
5-1. IDisposableとは何か
IDisposableは、使い終わったリソースを明示的に解放するためのインターフェイスです。
C#public interface IDisposable
{
void Dispose();
}
IDisposableを実装したクラスは、利用者がDispose()を呼ぶことで、ファイル、ストリーム、ハンドルなどのリソースを明示的に解放できます。
Microsoftの公式ドキュメントでも、IDisposableインターフェイスの主な用途はアンマネージリソースを解放することだと説明されています。また、GCの発生タイミングは予測できず、GCはウィンドウハンドルや開いているファイル、ストリームなどのアンマネージリソースを直接把握できないとも説明されています。
5-2. Disposeとデストラクタの呼び出しタイミングの違い
Disposeとデストラクタの最大の違いは、呼び出しタイミングです。
| 項目 | Dispose | デストラクタ |
|---|---|---|
| 呼び出す人 | 開発者、using構文 | GC |
| タイミング | 明示的で予測しやすい | 不確定 |
| 主な用途 | 確実なリソース解放 | Dispose漏れの最後の保険 |
| マネージリソース解放 | 可能 | 原則避ける |
| 実務での優先度 | 高い | 低い |
たとえば、次のようにDisposeを明示的に呼ぶと、その時点でリソース解放処理を実行できます。
C#var stream = new FileStream("sample.txt", FileMode.Open);
try
{
// ファイルを使う
}
finally
{
stream.Dispose();
}
しかし、デストラクタはGCが判断するまで呼ばれません。したがって、「ファイルを今すぐ閉じたい」「DB接続を今すぐ解放したい」という用途には向いていません。
5-3. using文・using宣言で安全にリソースを解放する方法
C#では、IDisposableを実装したオブジェクトを安全に扱うためにusingを使います。
C#using (var stream = new FileStream("sample.txt", FileMode.Open))
{
// ファイルを使う
}
usingブロックを抜けると、自動的にDisposeが呼ばれます。例外が発生してもDisposeが呼ばれるため、手動でtry-finallyを書くより簡潔です。
C# 8.0以降では、using宣言も使えます。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
// ファイルを使う
using宣言の場合、宣言された変数はスコープの末尾で破棄されます。公式ドキュメントでも、using宣言で宣言されたローカル変数は、そのスコープの末尾で破棄されると説明されています。
5-4. デストラクタよりIDisposableが推奨される理由
デストラクタよりIDisposableが推奨される理由は、リソース解放のタイミングを明確にできるからです。
デストラクタは、いつ呼ばれるか分かりません。場合によってはアプリケーション終了時に実行されないこともあります。特に.NET 5以降では、アプリケーション終了の一部としてファイナライザーは呼び出されないと公式ドキュメントに記載されています。
一方、Disposeは開発者が明示的に呼べます。また、usingを使えばスコープ終了時に自動で呼べます。
そのため、実務では次の順で考えるのが基本です。
C#// 基本
using var resource = new SomeDisposableResource();
// 自作クラスでリソースを持つなら
public class MyResource : IDisposable
{
public void Dispose()
{
// リソース解放
}
}
デストラクタは、Disposeが呼ばれなかった場合に備える最後の安全装置として検討します。
6. Disposeパターンとデストラクタの正しい使い方
6-1. Disposeパターンの基本構造
IDisposableを実装するクラスでは、単にDispose()を書くだけでなく、継承や重複呼び出しを考慮した「Disposeパターン」を使うことがあります。
基本形は次のようになります。
C#public class MyResource : IDisposable
{
private bool _disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// マネージリソースを解放
}
// アンマネージリソースを解放
_disposed = true;
}
}
Microsoftのドキュメントでも、継承を考慮したIDisposable実装では、パブリックな非仮想Dispose()と、保護された仮想Dispose(bool disposing)を用意するパターンが示されています。
6-2. Dispose(bool disposing)の意味
Dispose(bool disposing)のdisposingは、「明示的なDispose呼び出しなのか、ファイナライザーからの呼び出しなのか」を区別するための引数です。
C#protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// Disposeから呼ばれた場合
// マネージリソースを解放してよい
}
// Disposeからでもデストラクタからでも
// アンマネージリソースは解放する
_disposed = true;
}
disposingがtrueの場合は、利用者がDispose()を呼んだケースです。この場合は、他のマネージオブジェクトもまだ安全に扱える前提で、マネージリソースを解放できます。
disposingがfalseの場合は、デストラクタから呼ばれたケースです。この場合は、マネージオブジェクトにはアクセスせず、アンマネージリソースだけを解放します。
6-3. GC.SuppressFinalizeを使う理由
GC.SuppressFinalize(this)は、「このオブジェクトのファイナライザーをもう呼ばなくてよい」とGCに伝えるために使います。
C#public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
すでにDispose()でリソース解放が完了しているなら、後からデストラクタを実行する必要はありません。そこでGC.SuppressFinalize(this)を呼び、不要なファイナライザー実行を抑制します。
Disposeパターンの公式サンプルでも、Dispose()の中でDispose(true)を呼び、その後にGC.SuppressFinalize(this)を呼ぶ形が示されています。
6-4. デストラクタとDisposeを組み合わせた実装例
アンマネージリソースを直接扱う場合、デストラクタとDisposeを組み合わせることがあります。
C#using System;
using System.Runtime.InteropServices;
public class NativeBuffer : IDisposable
{
private IntPtr _buffer;
private bool _disposed;
public NativeBuffer(int size)
{
_buffer = Marshal.AllocHGlobal(size);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (_buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_buffer);
_buffer = IntPtr.Zero;
}
_disposed = true;
}
~NativeBuffer()
{
Dispose(false);
}
}
この例では、Marshal.AllocHGlobalで確保したネイティブメモリをMarshal.FreeHGlobalで解放しています。
利用者は次のようにusingで使います。
C#using var buffer = new NativeBuffer(1024);
// bufferを使う
この場合、通常はDispose()が呼ばれてリソースが解放されます。もし利用者がDispose()を呼び忘れた場合でも、デストラクタが最後の保険として動く可能性があります。
ただし、これはあくまでアンマネージリソースを直接扱う場合の例です。通常のアプリケーションコードでここまで書く機会は多くありません。
6-5. SafeHandleを使う場合の考え方
アンマネージハンドルを扱う場合、現在の.NETでは自分でデストラクタを書くより、SafeHandleまたはその派生クラスを使うことが推奨されます。
公式ドキュメントでも、多くの場合はSystem.Runtime.InteropServices.SafeHandleまたは派生クラスを使ってアンマネージハンドルをラップすれば、ファイナライザーを書く必要はないと説明されています。
SafeHandleを使うと、危険なハンドル解放処理をより安全にカプセル化できます。
イメージとしては、次のように考えます。
C#public class MyResource : IDisposable
{
private SafeHandle? _handle;
public void Dispose()
{
_handle?.Dispose();
GC.SuppressFinalize(this);
}
}
実務では、可能な限り既存のSafeHandle派生クラスや、.NETライブラリが提供するIDisposable実装を使うのが安全です。
7. C#のデストラクタでよくある誤解と注意点
7-1. デストラクタはすぐに呼ばれるわけではない
C#のデストラクタで最も多い誤解は、「オブジェクトが不要になったらすぐ呼ばれる」というものです。
実際には、変数がスコープを抜けても、すぐにデストラクタが呼ばれるとは限りません。
C#static void Main()
{
Create();
Console.WriteLine("Create終了");
}
static void Create()
{
var sample = new Sample();
}
このコードでCreateメソッドが終わっても、Sampleのデストラクタが即座に呼ばれるとは限りません。
GCのタイミングはランタイムが判断します。開発者が「この行で必ずデストラクタが呼ばれる」と期待して設計してはいけません。
7-2. 呼び出し順序は保証されない
複数のオブジェクトがファイナライズ対象になった場合、その呼び出し順序に依存してはいけません。
たとえば、あるオブジェクトのデストラクタ内で、別のマネージオブジェクトを使おうとすると危険です。
C#class A
{
public void Write()
{
Console.WriteLine("A");
}
}
class B
{
private A _a = new A();
~B()
{
// このようなマネージオブジェクト利用は避ける
_a.Write();
}
}
ファイナライザー実行時には、関連するマネージオブジェクトがどのような状態か保証しにくいため、このような処理は避けるべきです。
7-3. デストラクタ内で例外を出してはいけない
デストラクタ内で例外を発生させる設計は避けるべきです。
C#~Sample()
{
throw new Exception("エラー"); // 絶対に避ける
}
ファイナライザーはGCの処理の一部として実行されます。ここで例外が発生すると、通常の処理フローで捕捉することが難しく、アプリケーション全体に悪影響を与える可能性があります。
どうしても後始末処理の失敗に備える必要がある場合でも、デストラクタに複雑な処理を入れるのではなく、Disposeで明示的に処理し、エラーもそこで扱う設計にしましょう。
7-4. デストラクタはパフォーマンスに影響する
デストラクタを持つオブジェクトは、通常のオブジェクトよりもGCの処理が複雑になります。
ファイナライザーが存在すると、オブジェクトはすぐにメモリ回収されず、ファイナライズ処理のためのキューに登録されます。不要なファイナライザーはパフォーマンス低下の原因になります。
特に、次のような空のデストラクタは避けましょう。
C#class Sample
{
~Sample()
{
// 何もしない
}
}
何もしないデストラクタは、メリットがないのにGCの負荷だけを増やします。
7-5. マネージリソースの解放にデストラクタを使わない
マネージリソースの解放にデストラクタを使うのは適切ではありません。
たとえば、次のようなコードは避けるべきです。
C#class BadExample
{
private StreamReader _reader = new StreamReader("sample.txt");
~BadExample()
{
_reader.Dispose(); // ファイナライザー内でマネージオブジェクトに触るのは避ける
}
}
StreamReaderはマネージオブジェクトです。こうしたリソースは、デストラクタではなくDisposeで解放します。
C#class GoodExample : IDisposable
{
private StreamReader _reader = new StreamReader("sample.txt");
public void Dispose()
{
_reader.Dispose();
}
}
そして利用側ではusingを使います。
C#using var example = new GoodExample();
8. C#のデストラクタが呼ばれない・動かないときの原因
8-1. 参照が残っていてGC対象になっていない
デストラクタが呼ばれない原因としてよくあるのが、まだどこかに参照が残っているケースです。
C#static Sample? _sample;
static void Main()
{
_sample = new Sample();
GC.Collect();
GC.WaitForPendingFinalizers();
}
この例では、_sampleに参照が残っているため、SampleインスタンスはGC対象になりません。そのため、デストラクタは呼ばれません。
イベント購読、staticフィールド、キャッシュ、シングルトンなどに参照が残っている場合も同様です。
8-2. GCのタイミングが未確定である
参照がなくなっていても、GCがまだ実行されていなければデストラクタは呼ばれません。
C#static void Main()
{
Create();
Console.WriteLine("終了");
}
static void Create()
{
var sample = new Sample();
}
このコードでは、Create終了後にsampleへの参照がなくなる可能性はありますが、GCが実行されるとは限りません。そのため、デストラクタのログが表示されないことがあります。
これは異常ではなく、C#の仕様に沿った動作です。
8-3. プログラム終了時に必ず実行されるとは限らない
「アプリケーション終了時にはデストラクタが必ず呼ばれる」と考えるのも誤解です。
公式ドキュメントでは、ファイナライザーがアプリケーション終了の一部として実行されるかどうかは.NETの実装によって異なり、.NET 5以降ではアプリケーション終了の一部としてファイナライザーは呼び出されないと説明されています。
そのため、終了時に必ず実行したい処理をデストラクタに置くべきではありません。
終了時に確実なクリーンアップが必要な場合は、アプリケーションの終了処理でDisposeを呼ぶ設計にします。
8-4. デバッグ時に確認しづらい理由
デバッグ中にデストラクタの動作を確認しづらい理由はいくつかあります。
まず、デバッガーが変数の寿命に影響を与えることがあります。ローカル変数が思ったより長く生存しているように見えることもあります。
また、JITコンパイラーの最適化やビルド構成によっても、オブジェクトがGC対象になるタイミングは変わることがあります。
そのため、デストラクタの確認コードは実行結果が環境によって変わる可能性があります。
学習用に確認する場合は、次のようにメソッドを分け、GC.Collect()とGC.WaitForPendingFinalizers()を使います。
C#static void Main()
{
CreateObject();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("終了");
}
static void CreateObject()
{
var sample = new Sample();
}
ただし、これはあくまで確認用です。実務コードではGCを強制せず、Dispose設計を優先します。
8-5. 呼び出し確認よりDispose設計を優先すべき理由
デストラクタが呼ばれるかどうかを確認することに時間を使うより、Disposeが確実に呼ばれる設計にするほうが重要です。
たとえば、次のような設計にします。
C#public class MyService : IDisposable
{
private readonly FileStream _stream = new FileStream("sample.txt", FileMode.OpenOrCreate);
public void Dispose()
{
_stream.Dispose();
}
}
利用側は次のようにします。
C#using var service = new MyService();
これならスコープ終了時にDisposeが呼ばれます。デストラクタの不確定なタイミングに依存する必要がありません。
9. 初心者が押さえるべきデストラクタの実務的な判断基準
9-1. 基本はデストラクタを書かない
初心者がまず覚えるべき判断基準は、「基本はデストラクタを書かない」です。
C#では、通常のメモリ管理はGCが担当します。マネージオブジェクトだけを扱うクラスにデストラクタを書く必要はありません。
C#public class Product
{
public string Name { get; set; } = "";
public int Price { get; set; }
}
このようなクラスにデストラクタは不要です。
むしろ、不要なデストラクタを書くとGCの効率が下がる可能性があります。
9-2. リソース解放はIDisposableとusingを使う
ファイル、ストリーム、DB接続などを扱うときは、デストラクタではなくIDisposableとusingを使います。
C#using var connection = new SqlConnection(connectionString);
connection.Open();
// DB処理
usingを使うことで、処理が終わったあとにDisposeが呼ばれます。
IDisposableを実装するオブジェクトを使うだけであれば、利用者側は基本的にusingを使えば十分です。公式ドキュメントでも、IDisposableを実装するオブジェクトを使う場合は、使用完了時にDispose実装を呼び出す必要があり、C#ではusingステートメントなどの言語構文を使用できると説明されています。
9-3. アンマネージリソースを直接扱う場合だけ検討する
デストラクタを検討するのは、アンマネージリソースを直接扱う場合です。
たとえば、次のようなケースです。
C#private IntPtr _nativeMemory;
IntPtrでネイティブメモリやネイティブハンドルを保持している場合、それを解放する責任が自分のクラスにあります。
ただし、この場合でも最初に検討すべきはSafeHandleや既存ライブラリの利用です。自分でファイナライザーを書くのは最後の手段と考えましょう。
9-4. ライブラリやフレームワークのDispose仕様を確認する
実務では、自分でリソース管理クラスを作るより、既存のライブラリやフレームワークを使うことが多いです。
その場合は、そのクラスがIDisposableを実装しているかを確認します。
C#public class SomeClient : IDisposable
{
public void Dispose()
{
// リソース解放
}
}
IDisposableを実装しているクラスは、原則として使い終わったらDisposeする必要があります。
利用側では次のように書きます。
C#using var client = new SomeClient();
デストラクタを書くかどうかを考える前に、「このクラスはDisposeが必要か」「公式ドキュメントではどう使うように書かれているか」を確認しましょう。
10. C#のデストラクタに関するよくある質問
10-1. C#にデストラクタは本当に必要?
必要になる場面はありますが、かなり限定的です。
C#の通常のクラスでは、メモリ管理はGCに任せます。デストラクタが必要になるのは、アンマネージリソースを直接扱い、Disposeが呼ばれなかった場合の最後の保険が必要な場合です。
ほとんどのアプリケーション開発では、デストラクタを自分で書く機会は多くありません。
10-2. デストラクタとコンストラクタの違いは?
コンストラクタは、オブジェクト生成時に呼ばれる初期化処理です。
C#class Sample
{
public Sample()
{
Console.WriteLine("生成時の処理");
}
}
デストラクタは、オブジェクトがGCによって回収されるときに呼ばれる可能性がある終了処理です。
C#class Sample
{
~Sample()
{
Console.WriteLine("終了時の処理");
}
}
違いをまとめると、次のようになります。
| 項目 | コンストラクタ | デストラクタ |
|---|---|---|
| 目的 | 初期化 | 最終クリーンアップ |
| 呼び出しタイミング | newしたとき | GCが判断したとき |
| 引数 | 指定できる | 指定できない |
| 修飾子 | 指定できる | 指定できない |
| 明示的呼び出し | 生成時に呼ばれる | 直接呼べない |
コンストラクタは日常的に使いますが、デストラクタは慎重に使う必要があります。
10-3. Disposeだけ書けばデストラクタは不要?
多くの場合、Disposeだけで十分です。
特に、クラスが所有しているのがマネージリソースだけであれば、デストラクタは不要です。
C#public class ManagedResourceHolder : IDisposable
{
private readonly MemoryStream _stream = new MemoryStream();
public void Dispose()
{
_stream.Dispose();
}
}
この例では、MemoryStreamというマネージオブジェクトをDisposeしています。デストラクタは必要ありません。
一方、ネイティブメモリやOSハンドルを直接所有している場合は、Dispose漏れに備えてデストラクタを検討することがあります。
10-4. FinalizeとDisposeはどちらを使うべき?
基本的にはDisposeを使うべきです。
Finalize、つまりC#のデストラクタは、呼び出しタイミングが不確定です。リソースを確実に解放したい処理には向いていません。
Disposeは明示的に呼べます。さらにusingを使えば、スコープを抜けるときに自動的に呼ばれます。
C#using var resource = new MyResource();
そのため、実務では次のように考えます。
| 状況 | 選ぶべき方法 |
|---|---|
| 通常のリソース解放 | Dispose |
| 利用側で安全に解放 | using |
| Dispose漏れの最後の保険 | デストラクタ |
| アンマネージハンドル管理 | SafeHandle優先 |
10-5. デストラクタでログ出力してもよい?
学習や一時的なデバッグ目的であれば、デストラクタにログ出力を書いて動作確認することはあります。
C#~Sample()
{
Console.WriteLine("デストラクタが呼ばれました");
}
ただし、実務コードでログ出力を前提にするのはおすすめできません。理由は、デストラクタがいつ呼ばれるか分からず、アプリケーション終了時に必ず実行されるとも限らないためです。
ログを確実に出したいなら、Disposeや明示的な終了処理の中で出力しましょう。
C#public void Dispose()
{
Console.WriteLine("リソースを解放しました");
}
まとめ
C#のデストラクタは、~クラス名()で定義する特別な終了処理です。ただし、C++のデストラクタのように、スコープを抜けたタイミングですぐ呼ばれるものではありません。C#ではGCがオブジェクトを回収するときに、必要に応じてファイナライザーとして実行されます。
重要なポイントは次のとおりです。
| ポイント | 内容 |
|---|---|
| C#のデストラクタ | 現在はファイナライザーと呼ばれることが多い |
| 呼び出しタイミング | GCが決めるため不確定 |
| 通常のクラス | デストラクタは不要 |
| リソース解放 | IDisposableとusingを優先 |
| Finalizeとの関係 | デストラクタ構文はFinalize相当の処理に変換される |
| 実務での判断 | アンマネージリソースを直接扱う場合だけ検討 |
| 推奨設計 | Dispose、using、SafeHandleを使う |
初心者のうちは、「C#では基本的にデストラクタを書かない」「リソース解放はIDisposableとusingで行う」と覚えておくのが安全です。
デストラクタは便利な後始末機能ではなく、アンマネージリソースを扱う高度な場面で使う最後の安全装置です。まずはDisposeとusingを正しく理解し、必要な場合だけデストラクタを検討するようにしましょう。

