C#にdeleteはない?newしたオブジェクトの解放・GC・Disposeの正しい考え方
はじめに
C#を学び始めた人、とくにC++を経験している人が戸惑いやすいのが「newしたオブジェクトはどうやって解放するのか」という点です。
C++では、newで確保したメモリはdeleteで解放する、という考え方が基本です。そのため、C#でも同じように「newしたらdeleteしなければならないのでは?」と考えるのは自然です。
しかし、C#にはC++のようなdelete演算子はありません。C#では、通常のオブジェクトのメモリ解放はガベージコレクション、つまりGCが自動的に行います。
一方で、C#にも「明示的に解放すべきもの」は存在します。それがファイル、データベース接続、ネットワーク接続、OSハンドルなどのリソースです。これらはdeleteではなく、Disposeやusingを使って解放します。
この記事では、C#におけるnew、deleteがない理由、GC、Dispose、using、null代入の違いを整理しながら、C#でオブジェクトやリソースを正しく扱う考え方を解説します。
1. C#にdeleteはない?結論と検索意図の整理
1-1. C#ではC++のdeleteのように明示的にメモリ解放しない
結論から言うと、C#にはC++のdeleteに相当する構文はありません。
C++では次のように、newで確保したメモリをdeleteで解放します。
C++Sample* obj = new Sample();
delete obj;
一方、C#では次のようにnewでオブジェクトを作成しますが、対応するdeleteは書きません。
C#var obj = new Sample();
C#では、通常のマネージドオブジェクトのメモリ管理は.NETランタイムが担当します。開発者が「このオブジェクトのメモリを今すぐ解放する」と指定するのではなく、不要になったオブジェクトをGCが判断して回収します。
そのため、C#で次のようなコードは書けません。
C#// C#では不可
delete obj;
C#でdeleteしたいと感じた場合、まず考えるべきことは「メモリを解放したいのか」「ファイルやDB接続などのリソースを閉じたいのか」です。この2つは似ているようで、C#ではまったく別の問題として扱います。
1-2. newしたオブジェクトはGCが自動的に回収する
C#でnewしたオブジェクトは、不要になったタイミングでGCの回収対象になります。
たとえば、次のようなコードを考えます。
C#void Method()
{
var user = new User();
user.Name = "Taro";
}
userはメソッド内のローカル変数です。メソッドの実行が終わると、通常そのローカル変数は使われなくなります。ほかの場所からそのUserオブジェクトが参照されていなければ、そのオブジェクトはGCの回収対象になります。
重要なのは、「メソッドを抜けた瞬間に必ずメモリが解放される」という意味ではないことです。C#では、不要になったオブジェクトはGCの回収候補になりますが、実際にいつ回収されるかはGCの判断に任されます。
1-3. 「deleteしたい」と感じる場面はメモリ解放ではなくリソース解放の問題である
C#で「deleteしたい」と感じる場面の多くは、実際にはメモリ解放ではなくリソース解放の問題です。
たとえば、次のようなものを扱う場合です。
C#var stream = new FileStream("sample.txt", FileMode.Open);
このFileStreamは、単なるメモリ上のオブジェクトではありません。ファイルを開いているため、OS側のファイルハンドルというリソースを保持しています。
この場合、必要なのはdeleteではなくDisposeです。
C#stream.Dispose();
または、より安全に次のようにusingを使います。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
このように、C#では「メモリはGC」「リソースはDispose」という役割分担で考えることが重要です。
1-4. この記事で解決する疑問:GC・Dispose・using・null代入の違い
C#のnewやdeleteについて調べていると、次のような疑問が出てきます。
「C#でdeleteの代わりになる構文はあるのか」
「newしたオブジェクトはいつ解放されるのか」
「nullを代入すればメモリは解放されるのか」
「Disposeを呼べばメモリが解放されるのか」
「usingは何をしているのか」
「GC.Collectを呼べばよいのか」
これらはすべて、C#のメモリ管理とリソース管理を理解するうえで重要な疑問です。以降で順番に整理していきます。
2. C#のnewは何をしているのか
2-1. newでオブジェクトがヒープ上に作成される仕組み
C#でクラスのインスタンスを作成するときは、通常newを使います。
C#var person = new Person();
このコードでは、Personクラスのオブジェクトが作成されます。クラスは参照型なので、作成されたオブジェクトは基本的にマネージドヒープ上に配置されます。
マネージドヒープとは、.NETランタイムが管理するメモリ領域です。C#の開発者が直接メモリ番地を管理するのではなく、ランタイムとGCがオブジェクトの割り当てや回収を管理します。
C++のように、newで確保した領域を自分でdeleteする必要はありません。C#のnewは「手動解放が必要なメモリを確保する命令」ではなく、「ランタイム管理下にオブジェクトを作成する命令」と考えると理解しやすくなります。
2-2. 変数にはオブジェクトそのものではなく参照が入る
C#の参照型では、変数にオブジェクトそのものが入っているわけではありません。変数には、オブジェクトを指す参照が入っています。
C#var a = new Person();
var b = a;
この場合、aとbは同じPersonオブジェクトを参照しています。オブジェクトが2つ作られたわけではありません。
C#a.Name = "Taro";
Console.WriteLine(b.Name); // Taro
aを通じて変更した内容がbからも見えるのは、両方が同じオブジェクトを参照しているからです。
C#のメモリ管理では、「オブジェクトが存在するか」よりも「そのオブジェクトへの参照が残っているか」が重要です。どこからも参照されなくなったオブジェクトは、GCによって回収できる候補になります。
2-3. 参照型と値型でメモリ管理の考え方が違う
C#には大きく分けて参照型と値型があります。
クラス、配列、文字列、デリゲートなどは参照型です。
C#var user = new User();
var numbers = new int[] { 1, 2, 3 };
一方、int、double、bool、DateTime、structなどは値型です。
C#int count = 10;
DateTime now = DateTime.Now;
値型の変数には、基本的に値そのものが入ります。ローカル変数として使われる値型は、参照型とは異なる扱いを受けることがあります。
ただし、値型であっても、配列の要素、クラスのフィールド、ボックス化された値など、状況によってはヒープ上に配置されます。
重要なのは、C#では参照型であっても値型であっても、C++のように通常のコードでdeleteを使って解放することはない、という点です。
2-4. newしただけでは「自分で解放する責任」は発生しない
C#では、newしたからといって必ず自分で解放しなければならないわけではありません。
たとえば、次のような普通のクラスを考えます。
C#public class User
{
public string Name { get; set; } = "";
}
void Method()
{
var user = new User();
user.Name = "Taro";
}
このUserクラスは、特別な外部リソースを保持していません。単にメモリ上にデータを持つだけのオブジェクトです。
この場合、開発者が行うべき解放処理はありません。deleteもDisposeも不要です。参照されなくなれば、GCが必要に応じて回収します。
ただし、newしたオブジェクトがIDisposableを実装している場合は話が変わります。その場合は、メモリではなくリソースを解放するためにDisposeが必要になることがあります。
3. C#でオブジェクトが解放される仕組み:GCの基本
3-1. GCとは何か
GCとは、Garbage Collectionの略です。日本語ではガベージコレクションと呼ばれます。
C#におけるGCは、不要になったマネージドオブジェクトを自動的に回収する仕組みです。C++のように開発者がdeleteを書くのではなく、.NETランタイムがメモリの使用状況を監視し、不要なオブジェクトを見つけて回収します。
GCは、プログラムが使用しているオブジェクトと、もう使用されていないオブジェクトを判別します。使用されていないと判断されたオブジェクトは、将来的にメモリから回収されます。
この仕組みによって、C#ではdelete忘れによる典型的なメモリ解放漏れを避けやすくなっています。
3-2. 参照されなくなったオブジェクトが回収対象になる
GCがオブジェクトを回収できるかどうかは、そのオブジェクトが到達可能かどうかで判断されます。
たとえば、次のようなコードがあります。
C#void CreateUser()
{
var user = new User();
user.Name = "Taro";
}
このメソッドの実行中は、ローカル変数userからUserオブジェクトを参照できます。
しかし、メソッドを抜けるとuserは使われなくなります。ほかの変数やフィールド、コレクションなどからそのオブジェクトが参照されていなければ、GCはそのオブジェクトを回収可能と判断できます。
一方で、次のようにstaticフィールドに保存すると、参照が残り続けます。
C#static User? CachedUser;
void CreateUser()
{
CachedUser = new User();
}
この場合、CachedUserが参照を保持しているため、オブジェクトは不要に見えてもGCの回収対象になりません。C#のメモリリークは、このように「参照が残り続ける」ことで発生することが多いです。
3-3. GCが実行されるタイミングは開発者が基本的に制御しない
C#では、オブジェクトが不要になったとしても、すぐにGCが実行されるとは限りません。
GCは、メモリの使用状況や世代別管理、アプリケーションの状態などをもとに、ランタイムが適切と判断したタイミングで実行されます。
つまり、次のようなコードを書いたとしても、
C#var user = new User();
user = null;
user = null;を書いた瞬間に、必ずメモリが解放されるわけではありません。これは単に、変数userがオブジェクトを参照しなくなるだけです。
実際にメモリが回収されるかどうか、またそのタイミングはGCに任されます。
3-4. GC.Collectを安易に呼ぶべきではない理由
C#には、明示的にGCの実行を要求するGC.Collectというメソッドがあります。
C#GC.Collect();
しかし、通常のアプリケーションコードでGC.Collectを安易に呼ぶべきではありません。
GCは、ランタイムが効率よく動作するように設計されています。開発者が頻繁にGC.Collectを呼ぶと、本来不要なタイミングでGCが実行され、パフォーマンスが悪化することがあります。
また、GC.Collectを呼んだからといって、すべての不要そうに見えるオブジェクトが即座に完全に解放されるとは限りません。finalizerを持つオブジェクトなどは、回収までに追加の段階を経ることもあります。
GC.Collectは、特殊な事情がある場合に慎重に使うものです。通常は、GCに任せるのが基本です。
3-5. メモリリークの原因はdelete忘れではなく参照の残り続けである
C++では、newしたメモリをdeleteし忘れるとメモリリークになります。
一方、C#では通常のオブジェクトに対してdeleteを書く必要がありません。そのため、C#におけるメモリリークの主な原因は「delete忘れ」ではありません。
C#で問題になるのは、不要になったオブジェクトへの参照をどこかに残し続けることです。
代表的な例は次のようなものです。
C#static List<User> Users = new();
void AddUser()
{
Users.Add(new User());
}
このコードでは、UsersというstaticなリストがUserオブジェクトを保持し続けます。不要になってもリストから削除されなければ、GCはそれらのオブジェクトを回収できません。
C#でメモリリークを防ぐには、「どこから参照され続けているのか」を意識する必要があります。
4. C#でdeleteの代わりに使うべき考え方
4-1. 単なるオブジェクトは何もしなくてよい
C#で普通のクラスをnewしただけであれば、基本的に何もする必要はありません。
C#var product = new Product
{
Name = "Keyboard",
Price = 5000
};
このProductオブジェクトが単にメモリ上にデータを持つだけであれば、deleteもDisposeも不要です。
不要になったら、そのオブジェクトを参照している変数やフィールドが自然に使われなくなるのを待てば十分です。GCが適切なタイミングで回収します。
C#では「newしたら解放する」ではなく、「参照が不要になればGCに任せる」と考えます。
4-2. 不要になった参照を外せばGCの回収対象になる
オブジェクトを不要にしたい場合は、そのオブジェクトへの参照を持ち続けないことが重要です。
C#User? user = new User();
user.Name = "Taro";
// 以降不要
user = null;
このようにnullを代入すると、変数userはオブジェクトを参照しなくなります。ほかに参照がなければ、そのオブジェクトはGCの回収対象になります。
ただし、ローカル変数であれば、多くの場合は明示的にnullを代入する必要はありません。スコープを抜ければ自然に参照は不要になるからです。
C#void Method()
{
var user = new User();
// 処理
} // このスコープを抜けるとuserは不要になる
明示的なnull代入が意味を持つのは、長く生きるフィールドやコレクションなどが参照を保持している場合です。
4-3. null代入はdeleteではない
null代入は、C#におけるdeleteではありません。
C#user = null;
このコードが行っているのは、「変数userがオブジェクトを参照しない状態にする」ことだけです。
オブジェクトそのものを即座に破棄しているわけではありません。メモリをその場で解放しているわけでもありません。GCが後で回収できる状態に近づけているだけです。
また、ほかの変数が同じオブジェクトを参照している場合、user = null;をしてもオブジェクトは回収対象になりません。
C#var user1 = new User();
var user2 = user1;
user1 = null;
// user2がまだ参照しているため、オブジェクトは不要ではない
Console.WriteLine(user2.Name);
このように、C#では「変数」と「オブジェクト」を分けて考える必要があります。
4-4. スコープを抜けるとローカル変数の参照は不要になる
ローカル変数の場合、スコープを抜ければその変数は基本的に使われなくなります。
C#void Process()
{
var item = new Item();
item.Execute();
}
このメソッドが終了すると、itemというローカル変数は不要になります。ほかの場所にitemの参照を渡していなければ、ItemオブジェクトはGCの回収対象になります。
そのため、次のようにメソッドの最後で毎回nullを代入する必要は通常ありません。
C#void Process()
{
var item = new Item();
item.Execute();
// 通常は不要
item = null;
}
C#では、ローカル変数の寿命はスコープで管理されます。不要なnull代入を増やすよりも、変数のスコープを適切に小さく保つほうが読みやすく、管理しやすいコードになります。
4-5. static変数やコレクションに残った参照には注意する
C#でメモリが解放されない原因になりやすいのは、static変数やコレクションに参照が残り続けるケースです。
C#static List<byte[]> Cache = new();
void AddData()
{
Cache.Add(new byte[1024 * 1024]);
}
このコードでは、1MBの配列がCacheに追加され続けます。Cacheがstaticなので、アプリケーションが動いている間、参照が残り続ける可能性があります。
不要になったデータは、リストから削除するか、Clearする必要があります。
C#Cache.Clear();
ただし、Clearもメモリを即座にOSへ返すという意味ではありません。リストが保持していた各オブジェクトへの参照を外し、GCが回収できる状態にするという意味です。
5. Disposeが必要なケース:メモリではなくリソースを解放する
5-1. Disposeは何を解放するためのものか
Disposeは、主にメモリ以外のリソースを解放するための仕組みです。
C#の通常のオブジェクトのメモリはGCが管理します。しかし、オブジェクトがOSのファイルハンドル、データベース接続、ソケット、画像リソース、ネイティブメモリなどを保持している場合、それらはGCだけに任せるべきではありません。
このような外部リソースを明示的に解放するために、IDisposableインターフェイスとDisposeメソッドがあります。
C#public interface IDisposable
{
void Dispose();
}
Disposeは、「このオブジェクトが保持しているリソースをもう使わないので解放してください」という意味を持ちます。
5-2. ファイル・DB接続・ネットワーク・画像・ハンドルなどは明示的な解放が必要
次のようなオブジェクトは、使い終わったら明示的に解放する必要があります。
ファイルを扱うFileStream。
C#var stream = new FileStream("sample.txt", FileMode.Open);
データベース接続を扱うSqlConnection。
C#var connection = new SqlConnection(connectionString);
ネットワーク接続を扱うクラス。
C#var client = new HttpClient();
画像や描画リソースを扱うクラス。
C#var image = Image.FromFile("sample.png");
これらは、単にC#のメモリ上に存在するだけではなく、外部の限られたリソースを保持します。放置すると、ファイルが閉じられない、接続が解放されない、ハンドルが不足する、といった問題が発生します。
5-3. IDisposableを実装している型はDispose対象
C#では、IDisposableを実装している型は、使い終わったらDisposeする対象です。
C#public class ResourceHolder : IDisposable
{
public void Dispose()
{
// リソース解放処理
}
}
利用側では、次のようにDisposeを呼びます。
C#var holder = new ResourceHolder();
holder.Dispose();
ただし、実際のコードでは直接Disposeを書くよりも、usingを使うのが一般的です。
C#using var holder = new ResourceHolder();
IDisposableを実装しているかどうかは、Visual StudioなどのIDEで型定義を確認したり、公式ドキュメントを確認したりすることで判断できます。
目安として、ファイル、DB、ネットワーク、ストリーム、画像、暗号化、圧縮、ネイティブリソースに関わる型はIDisposableであることが多いです。
5-4. Disposeしてもオブジェクトのメモリを直接解放するわけではない
よくある誤解として、「Disposeを呼ぶとオブジェクトのメモリが解放される」と考えてしまうケースがあります。
しかし、DisposeはC++のdeleteではありません。
C#var stream = new FileStream("sample.txt", FileMode.Open);
stream.Dispose();
このコードで解放されるのは、FileStreamが保持しているファイルハンドルなどのリソースです。FileStreamオブジェクト自体のマネージドメモリをその場で直接解放しているわけではありません。
Dispose後のオブジェクトも、参照が残っていれば変数としては存在します。
C#stream.Dispose();
// Dispose後に使うと例外になる可能性がある
// stream.Read(...);
メモリの回収はあくまでGCの仕事です。Disposeはリソースの解放、GCはメモリの回収、と分けて理解しましょう。
5-5. Dispose忘れで起こる代表的な不具合
Disposeを忘れると、さまざまな不具合が起こる可能性があります。
たとえば、ファイルを開いたままにすると、別の処理からそのファイルを編集・削除できないことがあります。
C#var stream = new FileStream("sample.txt", FileMode.Open);
// Disposeしないまま放置
データベース接続を閉じ忘れると、接続プールが枯渇し、新しい接続を開けなくなることがあります。
画像やハンドルを解放しないと、メモリ使用量やハンドル数が増え続け、アプリケーションの動作が不安定になることがあります。
このような問題は、deleteがないから起きるのではなく、Disposeすべきリソースを解放していないことが原因です。
6. using文・using宣言で安全にDisposeする方法
6-1. using文の基本構文
C#では、IDisposableを実装しているオブジェクトを安全に解放するためにusing文を使えます。
C#using (var stream = new FileStream("sample.txt", FileMode.Open))
{
// streamを使う処理
}
この構文では、usingブロックを抜けるときに自動的にDisposeが呼ばれます。
たとえブロック内で例外が発生した場合でも、Disposeが呼ばれるようになっています。そのため、ファイルやDB接続などのリソースを扱うときは、手動でDisposeを書くよりも安全です。
6-2. using宣言の基本構文
C# 8.0以降では、using宣言も使えます。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
// streamを使う処理
using宣言では、明示的なブロックを書かなくても、その変数のスコープを抜けるときに自動でDisposeが呼ばれます。
たとえば、次のコードでは、メソッドを抜けるタイミングでstream.Dispose()が呼ばれます。
C#void ReadFile()
{
using var stream = new FileStream("sample.txt", FileMode.Open);
// streamを使う処理
}
using文とusing宣言は、どちらもDisposeを確実に呼ぶための仕組みです。スコープを明確にしたい場合はusing文、コードを簡潔にしたい場合はusing宣言が便利です。
6-3. usingを使うとスコープ終了時に自動でDisposeされる
usingの本質は、スコープ終了時に自動でDisposeすることです。
C#using (var resource = new ResourceHolder())
{
resource.DoSomething();
} // ここでDisposeされる
これは、次のような処理を安全に書いているのと同じような意味になります。
C#var resource = new ResourceHolder();
try
{
resource.DoSomething();
}
finally
{
resource.Dispose();
}
finallyに書かれた処理は、例外が発生しても実行されます。そのため、usingを使えば、正常終了でも例外発生時でもリソースを解放できます。
6-4. try-finallyでDisposeする書き方との関係
usingは、内部的にはtry-finallyに近い考え方です。
手動で書くと次のようになります。
C#FileStream? stream = null;
try
{
stream = new FileStream("sample.txt", FileMode.Open);
// streamを使う処理
}
finally
{
stream?.Dispose();
}
このコードは正しく動作しますが、毎回このように書くのは面倒です。また、書き忘れやミスも起こりやすくなります。
そこで、C#ではusingを使います。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
// streamを使う処理
usingを使うことで、リソース解放の意図が明確になり、コードも簡潔になります。
6-5. FileStreamやSqlConnectionでの実践例
ファイルを読み込む場合の例です。
C#using var stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
using var reader = new StreamReader(stream);
string text = reader.ReadToEnd();
Console.WriteLine(text);
このコードでは、readerとstreamがスコープ終了時にDisposeされます。
データベース接続の場合は、次のように書きます。
C#using var connection = new SqlConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT Name FROM Users";
using var reader = command.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader.GetString(0));
}
SqlConnection、SqlCommand、SqlDataReaderなどは、リソースを保持するため、使い終わったらDisposeする必要があります。usingを使うことで、接続やリーダーの閉じ忘れを防げます。
7. finalizer・デストラクタとdeleteの違い
7-1. C#のデストラクタはC++のdeleteとは別物
C#には、デストラクタのように見える構文があります。
C#class Sample
{
~Sample()
{
// finalizer
}
}
C#ではこの構文をデストラクタと呼ぶことがありますが、C++のデストラクタやdeleteとは大きく違います。
C++では、deleteを呼ぶとデストラクタが実行され、メモリ解放処理が行われます。
一方、C#の~Sample()はfinalizerとして扱われます。これはGCの過程で呼ばれる可能性があるものであり、開発者が任意のタイミングで呼ぶものではありません。
7-2. finalizerはGCによって呼ばれる
C#のfinalizerは、オブジェクトがGCによって回収される過程で呼ばれます。
C#class NativeResourceHolder
{
~NativeResourceHolder()
{
// ネイティブリソースの解放など
}
}
finalizerを持つオブジェクトは、通常のオブジェクトより回収に手間がかかります。GCがオブジェクトを発見したあと、finalizerを実行するための処理を経るためです。
そのため、finalizerは必要な場合だけ使うべきです。
7-3. finalizerの実行タイミングは保証されない
finalizerの大きな注意点は、実行タイミングが保証されないことです。
オブジェクトが不要になったからといって、すぐにfinalizerが呼ばれるわけではありません。GCが実行されるタイミングに依存します。
そのため、ファイルやDB接続など、できるだけ早く解放すべきリソースをfinalizer任せにするのは危険です。
C#class BadExample
{
~BadExample()
{
// ここでファイルを閉じればよい、という考え方は危険
}
}
リソースは、finalizerではなくDisposeとusingで明示的に解放するのが基本です。
7-4. finalizerを安易に書くべきではない理由
finalizerを安易に書くべきではない理由はいくつかあります。
まず、finalizerを持つオブジェクトはGCの負荷が増えます。通常のオブジェクトよりも回収に時間がかかる可能性があります。
また、finalizerの実行順序は保証されません。finalizer内でほかのマネージドオブジェクトに依存した処理を書くと、意図しない問題が起きることがあります。
さらに、finalizerは例外処理やリソース管理を複雑にします。通常のアプリケーション開発では、自分でfinalizerを書く必要はほとんどありません。
finalizerは、ネイティブリソースを直接保持するような低レベルなクラスを作る場合に検討するものです。
7-5. Disposeパターンが必要になるケース
ネイティブリソースやアンマネージドリソースを直接扱うクラスでは、Disposeパターンが必要になることがあります。
簡略化した例は次のとおりです。
C#public class ResourceHolder : 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;
}
~ResourceHolder()
{
Dispose(false);
}
}
ただし、通常の業務アプリケーションで毎回このようなコードを書く必要はありません。多くの場合は、既存のIDisposable型をusingで使えば十分です。
自分でIDisposableを実装する必要があるのは、自作クラスがIDisposableなフィールドを所有している場合や、アンマネージドリソースを直接扱う場合です。
8. C++経験者が混同しやすいC#のメモリ管理
8-1. C++のnew/deleteとC#のnew/GCの違い
C++とC#では、newの意味と責任範囲が大きく異なります。
C++では、newで確保したメモリは基本的にdeleteで解放します。
C++auto p = new Sample();
delete p;
一方、C#ではnewしたオブジェクトはGCの管理下に置かれます。
C#var obj = new Sample();
C#では、このあとにdelete obj;のようなコードは書きません。オブジェクトが参照されなくなれば、GCが回収します。
つまり、C++では「確保と解放の責任」が開発者にありますが、C#では「メモリ回収の責任」は主にランタイムにあります。
8-2. delete相当の構文がない理由
C#にdelete相当の構文がないのは、メモリ管理をGCが担う設計だからです。
もしC#に自由にメモリを破棄するdeleteがあると、ほかの参照がまだ残っているオブジェクトを破棄してしまう危険があります。
C#var a = new User();
var b = a;
// 仮にdeleteできたとすると、bはどうなるのか?
C#では複数の変数が同じオブジェクトを参照できます。そのため、ある変数から見て不要になったとしても、別の変数からはまだ必要かもしれません。
GCは、プログラム全体から見て到達可能かどうかを判断し、不要になったオブジェクトを回収します。この仕組みにより、手動解放による危険なバグを減らしています。
8-3. RAIIとusingの考え方の違い
C++では、RAIIという考え方がよく使われます。RAIIでは、オブジェクトの寿命とリソースの寿命を結びつけ、スコープを抜けたときにデストラクタでリソースを解放します。
C#にもusingがあり、スコープを抜けたときにDisposeを呼ぶという点では似ています。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
ただし、C#のusingはC++のRAIIと完全に同じではありません。
C++ではスコープを抜けるとデストラクタが確定的に呼ばれます。一方、C#の通常のオブジェクトは、スコープを抜けてもすぐに破棄されるとは限りません。
C#で確定的にリソース解放を行いたい場合は、デストラクタではなくDisposeとusingを使います。
8-4. ポインタ管理ではなく参照管理で考える
C#では、通常のコードでポインタを直接扱うことはほとんどありません。代わりに参照を扱います。
C#var user = new User();
このuserは、オブジェクトへの参照です。C++の生ポインタのように、手動でメモリ解放するものではありません。
C#で大切なのは、「誰がこのオブジェクトを参照しているか」を考えることです。
不要になったはずのオブジェクトが回収されない場合、どこかに参照が残っています。staticフィールド、イベント、キャッシュ、コレクション、クロージャなどが原因になりやすいです。
8-5. C#でメモリ解放を意識すべき場面
C#では通常、個々のオブジェクトのメモリ解放を細かく意識する必要はありません。
しかし、次のような場面ではメモリ管理を意識する必要があります。
大量のオブジェクトを生成する処理。
C#for (int i = 0; i < 1_000_000; i++)
{
var obj = new LargeObject();
}
大きな配列や画像データを扱う処理。
C#var buffer = new byte[100 * 1024 * 1024];
長期間生きるキャッシュやstaticフィールド。
C#static Dictionary<string, byte[]> Cache = new();
イベント購読やデリゲートで参照が残る処理。
C#publisher.SomeEvent += subscriber.Handle;
このような場面では、deleteを探すのではなく、参照を適切に手放すこと、Disposeすべきものを確実にDisposeすることが重要です。
9. よくある誤解と正しい対処法
9-1. 「newしたら必ず解放しなければならない」は誤解
C#では、newしたからといって必ず自分で解放する必要はありません。
C#var user = new User();
このような普通のオブジェクトは、参照されなくなればGCの回収対象になります。開発者がdeleteを書く必要はありません。
ただし、newしたオブジェクトがIDisposableを実装している場合は、使い終わったらDisposeする必要があります。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
正しくは、「newしたら必ず解放する」ではなく、「IDisposableなリソースはDisposeする」です。
9-2. 「nullを入れればすぐメモリが解放される」は誤解
nullを代入しても、すぐにメモリが解放されるわけではありません。
C#user = null;
これは、変数userがオブジェクトを参照しなくなるだけです。ほかに参照がなければGCの回収対象になりますが、実際にいつ回収されるかはGCの判断です。
また、別の変数やコレクションが参照を持っていれば、nullを代入しても回収されません。
C#var list = new List<User>();
var user = new User();
list.Add(user);
user = null;
// listが参照しているため、Userオブジェクトはまだ回収されない
null代入はdeleteではない、という点を押さえておきましょう。
9-3. 「Disposeすればメモリが解放される」は誤解
Disposeは、オブジェクトのマネージドメモリを直接解放するためのものではありません。
C#stream.Dispose();
このコードは、streamが保持しているファイルハンドルなどのリソースを解放します。しかし、streamオブジェクト自体のメモリはGCが回収します。
つまり、Disposeの目的はメモリ解放ではなく、リソース解放です。
Dispose後のオブジェクトを使おうとすると、例外が発生することがあります。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
// usingスコープ終了後に使うべきではない
9-4. 「GC.Collectを呼べば安全」は誤解
GC.Collectを呼べば安全になる、という考え方も誤解です。
C#GC.Collect();
このメソッドはGCの実行を要求しますが、通常のアプリケーションで頻繁に呼ぶものではありません。
不要なGC.Collectは、パフォーマンスを悪化させる可能性があります。また、根本的なメモリリーク、つまり参照が残り続けている問題は解決できません。
たとえば、staticリストに参照が残っている場合、GC.Collectを呼んでも回収されません。
C#static List<User> Users = new();
Users.Add(new User());
GC.Collect(); // Usersが参照しているため回収されない
メモリ使用量が気になる場合は、まず参照が残っていないかを確認するべきです。
9-5. 「メモリ使用量がすぐ下がらない=リーク」は誤解
C#アプリケーションでメモリ使用量を見ていると、処理が終わってもすぐに使用量が下がらないことがあります。
しかし、それだけでメモリリークとは限りません。
GCは必要に応じてメモリを回収しますが、回収したメモリをすぐにOSへ返すとは限りません。また、ランタイムが再利用のためにメモリ領域を保持することもあります。
本当に問題なのは、不要なオブジェクトが参照され続け、GCしても回収できない状態です。
メモリリークを疑う場合は、タスクマネージャーの数値だけで判断するのではなく、メモリプロファイラを使って、どのオブジェクトが残っているのか、どこから参照されているのかを確認することが重要です。
10. C#でメモリリークを防ぐ実践ポイント
10-1. 不要な参照を保持し続けない
C#でメモリリークを防ぐ基本は、不要な参照を保持し続けないことです。
C#private User? currentUser;
void Logout()
{
currentUser = null;
}
長く生きるフィールドが大きなオブジェクトを参照している場合、不要になったタイミングで参照を外す必要があります。
ただし、ローカル変数に対して何でもかんでもnullを代入する必要はありません。重要なのは、長期間生きるオブジェクトが不要な参照を持っていないかです。
10-2. イベント購読の解除漏れに注意する
C#でよくあるメモリリークの原因が、イベント購読の解除漏れです。
C#publisher.SomeEvent += subscriber.Handle;
この場合、publisherはsubscriberへの参照を保持します。publisherが長く生きるオブジェクトで、subscriberが不要になってもイベント購読が残っていると、subscriberはGCに回収されません。
不要になったら、イベント購読を解除します。
C#publisher.SomeEvent -= subscriber.Handle;
とくに、staticイベントやアプリケーション全体で生き続けるイベントに購読する場合は注意が必要です。
10-3. staticフィールドやシングルトンの使い方に注意する
staticフィールドやシングルトンは、アプリケーションの実行中ずっと生き続けることが多いです。そのため、そこに大きなオブジェクトや大量のデータを保持すると、GCで回収されません。
C#public static class GlobalCache
{
public static List<byte[]> Data { get; } = new();
}
このようなキャッシュは便利ですが、削除ルールや上限を設けないと、メモリ使用量が増え続けます。
不要なデータは削除する、一定数を超えたら古いものを捨てる、有効期限を設けるなどの設計が必要です。
10-4. 大きなコレクションは不要になったらClearする
大量のオブジェクトを保持しているコレクションは、不要になったらClearしましょう。
C#var users = new List<User>();
// 大量に追加
users.Clear();
Clearすると、リストが保持していた各要素への参照が外れます。ほかに参照がなければ、それらのオブジェクトはGCの回収対象になります。
ただし、Clearしてもリスト自体の内部配列の容量がすぐに縮むとは限りません。容量も減らしたい場合は、新しいリストを作り直す、TrimExcessを検討するなどの方法があります。
C#users.Clear();
users.TrimExcess();
10-5. IDisposableはusingで確実に解放する
IDisposableを実装している型は、原則としてusingで扱いましょう。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
usingを使えば、例外が発生してもDisposeされます。
次のように手動でDisposeを書くこともできます。
C#var stream = new FileStream("sample.txt", FileMode.Open);
try
{
// 処理
}
finally
{
stream.Dispose();
}
しかし、通常はusingのほうが簡潔で安全です。
C#でdeleteを探すよりも、「この型はIDisposableか」「usingで囲むべきか」を確認する習慣をつけるほうが重要です。
10-6. メモリプロファイラで原因を確認する
メモリリークが疑われる場合は、推測だけで対処しないことが大切です。
Visual Studioの診断ツール、dotMemory、PerfView、JetBrains Riderのプロファイラなどを使うと、どの型のオブジェクトが多く残っているか、どこから参照されているかを確認できます。
C#のメモリリーク調査では、「なぜGCされないのか」を追跡します。つまり、対象オブジェクトへの参照パスを調べます。
原因がイベント購読なのか、staticフィールドなのか、キャッシュなのか、コレクションなのかを確認してから修正するのが確実です。
11. C#でnewしたオブジェクトの解放に関する実例
11-1. 普通のクラスをnewした場合
普通のクラスをnewした場合です。
C#public class User
{
public string Name { get; set; } = "";
}
void Create()
{
var user = new User();
user.Name = "Taro";
}
このUserクラスは外部リソースを持っていません。そのため、deleteもDisposeも不要です。
メソッドを抜け、ほかに参照がなければ、UserオブジェクトはGCの回収対象になります。
明示的に次のような処理を書く必要はありません。
C#// 不要
user = null;
もちろん、長期間生きるフィールドに保持している場合は、不要になったタイミングで参照を外すことはあります。
11-2. FileStreamをnewした場合
FileStreamをnewした場合は、使い終わったらDisposeが必要です。
悪い例です。
C#var stream = new FileStream("sample.txt", FileMode.Open);
// Disposeしない
この場合、ファイルハンドルが解放されず、ファイルがロックされたままになる可能性があります。
正しい例です。
C#using var stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
// streamを使う処理
usingを使うことで、スコープ終了時に自動でDisposeされます。
ここで必要なのはdeleteではありません。ファイルという外部リソースを閉じるためのDisposeです。
11-3. SqlConnectionをnewした場合
データベース接続を扱うSqlConnectionも、使い終わったらDisposeする必要があります。
C#using var connection = new SqlConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM Users";
var count = (int)command.ExecuteScalar();
Console.WriteLine(count);
SqlConnectionをDisposeすると、接続が適切に閉じられ、接続プールに戻されます。
悪い例は、接続を開いたまま放置するコードです。
C#var connection = new SqlConnection(connectionString);
connection.Open();
// Disposeしない
これを繰り返すと、接続が不足してエラーになることがあります。C#でDB接続を扱う場合は、usingで囲むのが基本です。
11-4. Listに大量のオブジェクトを保持した場合
大量のオブジェクトをListに保持すると、そのリストが参照を持ち続ける限り、要素はGCされません。
C#var users = new List<User>();
for (int i = 0; i < 100000; i++)
{
users.Add(new User { Name = $"User{i}" });
}
この時点で、usersは大量のUserオブジェクトを参照しています。
不要になったら、参照を外します。
C#users.Clear();
または、リスト自体が不要なら、スコープを抜けるか、長く生きるフィールドであればnullを代入することもあります。
C#users = null;
ただし、Clearやnull代入はdeleteではありません。参照を外してGCが回収できる状態にする処理です。
11-5. イベント購読で参照が残る場合
イベント購読によって参照が残る例です。
C#public class Publisher
{
public event EventHandler? SomethingHappened;
public void Raise()
{
SomethingHappened?.Invoke(this, EventArgs.Empty);
}
}
public class Subscriber
{
public void Handle(object? sender, EventArgs e)
{
Console.WriteLine("Handled");
}
}
次のように購読します。
C#publisher.SomethingHappened += subscriber.Handle;
このとき、publisherはsubscriberのメソッドへの参照を持ちます。結果として、subscriberが不要になっても、publisherが生きている限りGCされないことがあります。
不要になったら解除します。
C#publisher.SomethingHappened -= subscriber.Handle;
C#のメモリリークでは、このようなイベント購読の解除漏れが原因になることがあります。
12. FAQ:C#のdelete・new・GC・Disposeに関するよくある質問
12-1. C#でdeleteの代わりになる構文はありますか?
C#にはC++のdeleteに相当する構文はありません。
通常のオブジェクトのメモリはGCが自動的に回収します。開発者が直接deleteする必要はありません。
リソースを解放したい場合は、deleteではなくDisposeやusingを使います。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
「C# delete」と検索している場合、多くはDispose、using、GCのどれかを理解することで解決できます。
12-2. newしたオブジェクトはいつ解放されますか?
newしたオブジェクトは、どこからも参照されなくなるとGCの回収対象になります。
ただし、実際にいつ回収されるかはGCの判断です。スコープを抜けた瞬間やnullを代入した瞬間に、必ずメモリが解放されるわけではありません。
C#void Method()
{
var user = new User();
} // ほかに参照がなければGCの回収対象
C#では、回収のタイミングを細かく制御するのではなく、不要な参照を残さないように設計することが大切です。
12-3. nullを代入すればメモリは解放されますか?
nullを代入しても、すぐにメモリが解放されるわけではありません。
C#user = null;
これは、変数userがオブジェクトを参照しなくなるだけです。ほかに参照がなければ、GCの回収対象になります。
ただし、別の変数やリスト、staticフィールドなどが参照を持っている場合、そのオブジェクトは回収されません。
C#var list = new List<User>();
var user = new User();
list.Add(user);
user = null;
// listが参照しているため回収されない
12-4. Disposeは必ず呼ぶべきですか?
IDisposableを実装しているオブジェクトは、基本的に使い終わったらDisposeすべきです。
ただし、すべてのnewしたオブジェクトにDisposeが必要なわけではありません。普通のクラスでIDisposableを実装していなければ、Disposeする必要はありません。
Disposeが必要な型は、次のようにusingで扱うのが安全です。
C#using var resource = new SomeDisposableResource();
Disposeが必要かどうか迷ったら、その型がIDisposableを実装しているかを確認しましょう。
12-5. GC.Collectは使ってもよいですか?
GC.Collectは存在しますが、通常のアプリケーションコードで安易に使うべきではありません。
C#GC.Collect();
GCはランタイムが適切なタイミングで実行します。開発者が頻繁に強制実行すると、パフォーマンスが悪化する可能性があります。
また、参照が残っているオブジェクトはGC.Collectを呼んでも回収されません。メモリリークを解決したい場合は、まず不要な参照が残っていないかを確認するべきです。
12-6. C#でもメモリリークは起こりますか?
はい、C#でもメモリリークは起こります。
ただし、C++のようなdelete忘れではなく、不要なオブジェクトへの参照が残り続けることが主な原因です。
代表的な原因は次のようなものです。
C#static List<object> Cache = new();
C#publisher.SomeEvent += subscriber.Handle;
staticフィールド、キャッシュ、イベント購読、長く生きるコレクションなどには注意が必要です。
12-7. デストラクタを書けばdeleteの代わりになりますか?
C#のデストラクタ、つまりfinalizerは、C++のdeleteの代わりにはなりません。
C#class Sample
{
~Sample()
{
// finalizer
}
}
finalizerはGCによって呼ばれるもので、実行タイミングは保証されません。そのため、ファイルやDB接続などのリソース解放をfinalizer任せにするべきではありません。
リソース解放にはDisposeとusingを使います。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
まとめ
C#には、C++のようなdeleteはありません。
C#でnewした通常のオブジェクトは、参照されなくなったあと、GCによって自動的に回収されます。そのため、普通のクラスをnewしただけであれば、開発者が明示的にメモリを解放する必要はありません。
一方で、ファイル、DB接続、ネットワーク、画像、ハンドルなどのリソースを扱うオブジェクトは、使い終わったらDisposeする必要があります。IDisposableを実装している型は、基本的にusingを使って安全に解放しましょう。
重要なポイントは、次の3つです。
C#のnewは、C++のnew/deleteとは考え方が違います。
通常のメモリ解放はGCに任せます。
外部リソースの解放はDisposeとusingで行います。
null代入はdeleteではありません。Disposeもメモリを直接解放するものではありません。GC.Collectも安易に呼ぶべきではありません。
C#でメモリ管理を正しく理解するには、「メモリはGC」「リソースはDispose」「参照を残し続けない」という考え方を押さえることが大切です。

