1. C#でリソースを正しく解放するには?
OSを厳しい図書館司書だと思ってみて。君が本を借りた(ファイルやストリームを開いた)、読んでる、でも…返すの忘れた!司書が怒る:「なんでまだ本返してないの!?」ストリームも同じ。開いたままだとリソース(ファイルディスクリプタ、メモリの一部、他アプリからのファイルロック)をずっと使いっぱなしになる。
ストリームを閉じないと、「なんか動かない」から「全部壊れた、誰もこのファイルに書き込めない」まで色々な問題が起きる。未クローズのストリームが多いと、システムがリソースリークして動かなくなることもあるよ。
どこが危険?
- ファイルが閉じられず、コマンドがディスクに届かない(例えば書き込み時、データがバッファに残ったまま)。
- ファイルが他プロセスにロックされる — 同僚や他のプログラムがイライラする。
- ディスクリプタの上限: Windows/Linuxではプロセスごとに開けるファイル/ストリーム数に制限がある。
IDisposableインターフェース
非管理リソース(ストリーム、ファイル、DB、ソケットなど)を扱うクラスは、IDisposableインターフェースを実装しなきゃダメ。
public interface IDisposable
{
void Dispose();
}
Dispose()メソッドの中で、全部のかわいそうなリソースを解放するのが普通:ファイルをやっと閉じたり、コネクション切ったり、メモリを解放したり。
ストリームは、ファイルやコネクション、メモリなど大事なリソースを持ってるオブジェクト。閉じないと、ファイルがロックされたまま(Wordが"ファイルは他のアプリで開かれてます!"って言うやつ)、システムがメモリ不足になる。だから、使い終わったら必ずストリームを解放しよう。
.NETのストリームはIDisposableを実装してる。つまり、Dispose()を呼ぶ(またはusingブロックに入れる)必要があるってこと。
2. ストリームのクローズ方法:危険から安心まで
パターン1. 「手動」クローズ:やめとけ!
これは古いやり方。前のサンプルで見せたけど、今どきはほぼ誰も使わない :P
var stream = new FileStream("file.txt", FileMode.Open);
// ストリームで作業
stream.Close(); // または stream.Dispose()
問題点:開いてから閉じるまでの間にエラーや例外が起きたら、ファイルが開きっぱなしでロックされる。図書館でピザの匂いに釣られて本持ったまま飛び出す感じ…
パターン2. try...finallyを使う
FileStream stream = null;
try
{
stream = new FileStream("file.txt", FileMode.Open);
// ストリームで作業
}
finally
{
if (stream != null)
stream.Dispose();
}
このやり方は安心:finallyはエラーがあっても必ず実行される。でも、正直書くのめんどい。
パターン3. かっこよく安全:using演算子
クラシック構文(using ( ... ) { ... })
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// ストリームで作業
}
// ここでstream.Dispose()が自動で呼ばれる!
ポイントは、usingブロックの中でストリームを使い、ブロックが終わったら(例外があっても)ファイルが閉じること。
パターン4. モダン構文
モダン構文(using var)
using var stream = new FileStream("file.txt", FileMode.Open);
// ストリームで作業
// ... 変数のスコープが終わったらDisposeが自動で呼ばれる
最高!余計なインデントや波カッコがいらない。
「中身」はどうなってる?
using演算子は、コンパイラがtry...finallyに展開してくれる。ジョーク:「usingは君の代わりにクリーンなコードを書く ― そのうちコーヒー飲みながらStack Overflow眺め始めるかも?」
クラシックとモダンusingの違い
| クラシックusingブロック | using var(宣言) | |
|---|---|---|
| 見た目 | |
|
| スコープ | ブロックの波カッコ内 | 今のブロック(メソッド、ループなど)の終わりまで |
| 簡潔さ | ちょっと冗長 | シンプル、インデント少なめ |
| 登場バージョン | C# 1.0 | C# 8.0以降 |
3. using宣言って何?
5年前、IDisposableオブジェクトを扱う新しいシンプルな方法が登場した。
using宣言は、ブロックじゃなくて変数宣言にusingを付けるやり方。変数は今のブロック(メソッドやループなど)の終わりで自動的に解放される。追加の波カッコブロックの終わりじゃないよ。
using var stream = new FileStream("file.txt", FileMode.Open);
// ストリームで作業
Console.WriteLine(stream.Length);
// ここではまだファイルは開いてる!
// ... メソッドの終わり
// stream.Dispose()がここで自動実行
クラシックusingとの主な違い:
- 波カッコがいらない、ネストしたコードブロックができない。
- 変数は宣言したブロックの終わりまで使える(普通はメソッド、時々ループやクラス(クラスレベルで宣言した場合))。
- リソース解放はブロックの終わりでだけ起きる。
なぜこれがイケてる?
- ネストが減る — コードがかなり短く読みやすくなる。
- 複数リソースも楽勝 — using変数を連続で宣言すれば、メソッドの終わりで全部解放される。
- ミスが減る — Dispose()を呼ぶべきスコープを間違えにくい。
4. 比較:クラシック vs. モダンusing
コードで比較してみよう。
クラシックなやり方
using (var reader = new StreamReader("input.txt"))
{
using (var writer = new StreamWriter("output.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
}
} // ここで両方のファイルが閉じられる
モダンなやり方(C# 8+)
using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
// メソッドを抜けると両方のファイルが閉じられる
シンプルでしょ? ネストがもっと深くなる場合は特に、モダンなやり方がめっちゃ楽。
5. Dispose()はいつ・どこで呼ばれる?
ここでよく学生がミスる:Disposeは使った直後に呼ばれると思いがち ― でも違うよ!
この例を見てみて:
void MyMethod()
{
using var fileStream = new FileStream("data.bin", FileMode.Open);
// ... たくさんのコード、ループやネストした呼び出しもあるかも
// fileStreamはまだ開いてる!
// ここでfileStreamにアクセスできる
}
// ここ、メソッドの}でfileStream.Dispose()が呼ばれる
大事なポイント: ループ内でusing変数を宣言すると、Disposeは各イテレーションの後に呼ばれる。
foreach (var path in filePaths)
{
using var reader = new StreamReader(path);
// readerで作業
} // 各イテレーション後にreader.Dispose()(ファイルを閉じる)
6. 古いコードを移植するときのミス
たまに、古いコードを移したりクラシックusingのサンプルをコピペしたりする時、変数の「寿命」が波カッコのスコープより長く必要な場合がある。そんな時はクラシックなやり方は向かないけど、using宣言ならバッチリ。
でも注意点もある。例えば、ループ内で2つのリソースがあって、一方はもう一方より長く「生きて」ほしい場合は、宣言の順番に気をつけて:
using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
using var resource2 = ...;
// resource2は1イテレーションだけ生きる
// resource1は関数全体で生きる
}
7. 実践
前回から作ってる学習用アプリ ― 小さなコーヒー注文シミュレーターを進化させよう。今度は注文履歴をテキストファイルに保存&起動時に読み込めるようにしよう。
ステップ1: 注文をファイルに保存
using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("コーヒー: ラテ; ミルク: オーツ; サイズ: ラージ");
// StreamWriterの2番目の引数append: trueは、ファイルを上書きじゃなく追記したいって意味。
ステップ2: 注文履歴を読む
using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"注文: {line}");
}
このメソッドが終われば、ファイルは自動で閉じられるよ。
8. using宣言のベストプラクティス
1. IDisposableを実装してるオブジェクトには必ずusingを使おう
.NETのファイル・ストリーム・リソース系クラスはほとんどこれ。つまり「usingで解放してね!」ってサイン。
2. スコープに注意:必要なとこだけでusing-varを宣言しよう
変数が数行だけ必要なら、その場で宣言して早めに解放しよう。
3. 解放順序を忘れずに
複数のusing変数を連続で宣言すると、Disposeは逆順で呼ばれる:
using var first = new Resource("First");
using var second = new Resource("Second");
// ... 作業
// 先にsecondのDispose、次にfirstのDispose
一方がもう一方に依存してる場合(例:書き込みストリームはファイルより先に解放すべき)には大事。
4. メソッド外でusing宣言は使わない
using宣言はクラスレベル(フィールドなど)では禁止。メソッドやコンストラクタなどの中だけで使える。
5. エラー処理と組み合わせよう
usingだけじゃ例外を全部カバーできないこともあるから、読み書きエラーをちゃんと制御したいならtry-catchも使おう。
GO TO FULL VERSION