1. はじめに: なぜファイルは「気まぐれ」するのか?
ドキュメントを開こうとしたら、突然システムがこう言うことがある:「ファイルが見つかりません」。あるいは保存しようとしたら:「アクセスが拒否されました」。これがまさにファイル周りで起きる変な状況だ。エンコーディングで「文字化け」に悩まされるのとは違い、ここではファイルシステム自体が問題を起こしていることが多い。
ファイルシステムは大きな図書館、アプリケーションは司書だと想像してみて。ファイルをリクエストすると色んな返答があり得る:
- その本(つまりファイル)がない — 単純に存在しない。
- その本があるはずの図書館の区画自体がない — 必要なディレクトリがない。
- 本が「鍵付」だったり誰かに貸し出されている — ファイルが他プロセスで使われている、またはアクセス権がない。
- 棚がいっぱい — 新しいファイルを作るためのディスク空き容量が足りない。
- 例えば本をDVD返却コーナーに置こうとするような、サポートされていない操作をしようとしている。
C#ではこれらの状況は例外として現れる。開発者の仕事はプログラムがただクラッシュするのを眺めることではなく、起こり得る問題を予見して丁寧に処理することだ。ユーザーに訳の分からないエラーメッセージを見せたくはないよね。
既に try-catch 構文には馴染みがあるはず。これは例外を「キャッチ」してプログラムの強制終了を防ぎ、代わりに適切な処理を行うための命綱だ。
// これはおなじみ、講義57からのリマインダー
try
{
// ここにエラーを引き起こす可能性のあるコードを書く
// 例えばファイルの読み取り
}
catch (Exception ex) // 任意の例外をキャッチ
{
// ここでエラーを処理する
Console.WriteLine($"おっと、エラーが発生しました: {ex.Message}");
}
今日はファイル操作に特有の例外に深堀りしていくよ。これでファイルシステムが「気まぐれ」しても、より堅牢なコードを書けるようになる。
例外はバグじゃない、救難信号だ!
重要なのは:例外は必ずしもコードのバグではないということ。多くの場合、それはコードがやり取りしている外部環境で何か問題が起きたことを知らせる信号だ。ファイルシステムはそういう外部環境の典型例。ファイルを読むためのコードが完璧でも、ユーザーがそのファイルをプログラムが読む前に削除してしまっていれば例外になる。それは正常なことだ。開発者としての役割は、そうした状況にプログラムを対応させることだ。
では、ファイル操作でよく見る「救難信号」を見ていこう。
2. FileNotFoundException: ファイルがそもそもない
ファイル操作で最もよく見る例外のひとつ。指定したパスにファイルが存在しないときに、開く・読む・その他操作をしようとすると発生する。
実生活の例:友達に「C# 14初心者のためのプログラミング」という本を持ってきてと頼んだら、友達が「そんな本持ってないよ」と返す。プログラムがOSに「settings.txtをくれ」と頼んだら、システムが「ごめん、それはないよ」と返すみたいなもの。
では、タスク管理アプリ用に abracadabra.txt を読むコードを書いてみよう。ファイルが無ければユーザーに知らせるべきだ。単に落ちてはいけない。
try
{
using var reader = new StreamReader("abracadabra.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません: " + ex.FileName);
}
日常では、バス停に行ったのにバスも代行タクシーも来ない、って感じかな。切ない。
注意: この例外はしばしばパスの間違い(例えば別のディレクトリで動いていることを忘れている)と一緒に出ることが多い。
3. DirectoryNotFoundException: ディレクトリがどこかへ消えた
これは FileNotFoundException に似ているけど、対象はファイルそのものではなく、そのファイルがあるはずのディレクトリだ。例えばパスが "C:\MyDocuments\MyProject\Data\report.txt" で、Data フォルダが存在しなければ DirectoryNotFoundException が出る。
アプリで設定を data サブフォルダ(例: "./data/app_settings.txt")に保存しようとして、そのフォルダが無ければ保存/読み取り時にこの問題に遭遇する。
DirectoryNotFoundException は FileNotFoundException と同様に個別に捕まえることもできるし、後で触れる一般的な IOException の一部として扱われることもある。
try
{
using var writer = new StreamWriter(@"C:\very\strange\path\file.txt");
writer.WriteLine("Hello world");
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine("ディレクトリが見つかりません!");
}
よくあるミス: フォルダはいつでも削除される可能性がある(例えば一時ファイルを誰かが消す)、あるいは書き込み先パスの設定を間違えている場合が多い。
4. UnauthorizedAccessException: 立ち入り禁止!
本を棚に置こうとしたら「職員専用」と書かれた札があるイメージ。これが UnauthorizedAccessException だ!プログラムにファイルやディレクトリへの必要なアクセス権がないときに発生する。原因としては:
- ユーザー権限が不十分: 管理者だけが書き込めるフォルダ(例: C:\Windows)に書き込もうとしている。
- ファイルが読み取り専用に設定されている: 読み取り専用属性のファイルを変更しようとしている。
- ファイルがシステムファイルや隠しファイルで特別扱いされている: 特定の制限が付いている。
企業環境や保護されたディレクトリにインストールされたプログラムでよく発生する問題だ。
システムフォルダにファイルを書き込もうとする例を見てみよう。(注意:こういうコードは権限に気をつけて、システム領域を汚さないようにサンドボックスで実行すること)
try
{
using var writer = new StreamWriter("/system/settings.conf");
writer.WriteLine("すべての力を学生に!");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("ファイルまたはディレクトリへのアクセスがありません!");
}
管理者権限なしで実行すると、たぶん 「このフォルダへのアクセスが拒否されました」 と言われる。管理者で実行すればファイルは作れるだろう。この例は、なぜ UnauthorizedAccessException を処理することが重要かを示している:ユーザーに操作ができない理由を知らせる必要があるからだ。
5. IOException: ファイルI/Oの万能「おっと」
IOException は入出力操作に関するもっとも一般的な例外。デバイスやファイルシステム自体の問題で、FileNotFoundException や UnauthorizedAccessException のようなより特化した例外に当てはまらないときに投げられることが多い。
典型的なシナリオ:
- ファイルが別のプログラムで使用中: 例えばファイルを削除しようとしたらNotepadや別のプログラムが開いている。
- ディスクが満杯: ファイルを書き込むのに十分な空き容量がない。
- 破損したファイルやファイルシステム: まれだが起こりうる。
- ネットワークの問題: ファイルがネットワークドライブ上にあり接続が切れた。
- ファイル名やパスが長すぎる: これは PathTooLongException の場合もあるが、時に IOException として扱われることもある。
IOException は多くの問題に対する「万能キー」のようなもの。捕まえたら、その Message プロパティを見て何が起きたか詳しく確認すると良い。
try
{
using var file = new FileStream("busyfile.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
// 何らかの理由でファイルをオープンしたままにする
// 別の場所で同時に:
using var writer = new StreamWriter("busyfile.txt");
writer.WriteLine("書き込みの試み...");
}
catch (IOException ex)
{
Console.WriteLine("入出力エラー: " + ex.Message);
}
重要な点: IOException は多くの「ファイル系」例外の基底クラスだ。
6. その他の、でも重要な「トラブル」
PathTooLongException
あまり頻繁ではないけど遭遇する問題:パス(またはファイル/ディレクトリ名)がOSの許容長を超えている。例えばファイル名に『戦争と平和』のあらすじを丸ごと入れようとすると、Windowsは許してくれないよ。
Windowsの歴史的制限はフルパスで 260 文字。ただし新しいOSや.NETは「長いパス」をサポートできるけど、デフォルトで有効とは限らない。
try
{
string veryLongPath = new string('a', 300); // 300文字!
using var writer = new StreamWriter(veryLongPath + ".txt");
writer.WriteLine("ファイル名が長すぎます!");
}
catch (PathTooLongException ex)
{
Console.WriteLine("ファイル名またはパスが長すぎます!");
}
NotSupportedException
これは稀だが「シュール」なケース:StreamReaderやFileStreamのコンストラクタに渡したパス文字列が不正(禁止文字を含む、あるいは魔法のようなパス C:::\wow???\file.txt を渡したなど)なときに出る。
7. 便利な注意点
どの例外がどの原因を表すか
| 例外 | 発生原因 | 状況の例 |
|---|---|---|
|
ファイルが見つからない | 存在しないファイルを開く |
|
ディレクトリが見つからない | 削除されたフォルダ内のファイルを開く |
|
アクセス権がない | 保護されたディレクトリへの書き込み |
|
一般的な入出力エラー | ファイルが別プロセスで開かれている |
|
パスが長すぎる | 長すぎるファイル/フォルダ名 |
|
不正なパス形式 | 禁止文字を含むパス |
よくあるミスと特殊ケース
例: ファイルが別のプロセスに占有されている
あなたがハッカー気取りでNotepadでファイルを開いたまま閉じ忘れたとしよう。その間にプログラムが同じファイルに書き込もうとすると、ここで IOException(あるいは "sharing violation")が返ってくる。
例: アクセス権がない
管理者権限なしで C:\Windows に保存しようとすると UnauthorizedAccessException が出る。読み取り専用で開いたファイルに書き込もうとしても同様だ。
例: 不正なパス
Windowsではファイル名に <>:"/\|?* のような文字を使えない。そんな名前でファイルを作ろうとすると NotSupportedException(または ArgumentException)が出る。
例: ディスクに空きがない
これも IOException を投げる — 例えばディスクがいっぱいのとき(定期的に Downloads フォルダを掃除しようね)。
やらかさないためのベストプラクティス
- ファイルを開く前に File.Exists や Directory.Exists で存在をチェックする。ただし注意:チェック後にファイルが消えたり出現したりする可能性がある(典型的な race condition)。
- 例外を丸のみしない(ただ catch { } をしない) — ログを残すかユーザーに何が起きたか伝えるようにする。
- 具体的な例外(FileNotFoundException、DirectoryNotFoundException など)をキャッチするようにして、単に一般的な Exception だけに頼らないこと。
- クロスプラットフォームアプリでは、アクセス権、パス形式、ファイル名の長さがOSごとに違うことを考慮すること。
- テキストファイルを扱うなら常にエンコーディングを明示する — サプライズを避けるため。
- 大量のファイル操作には「バルク」処理を考え、各ステップでの失敗を考慮した設計にする。
代表的な例外のまとめチートシート
| 例外 | いつ発生する? | どう予防/処理する? |
|---|---|---|
|
指定パスにファイルがない | File.Exists で確認するか、作成する |
|
パスに存在しないディレクトリが含まれる | パスをチェックし、Directory.CreateDirectory でディレクトリを作る |
|
ファイル/フォルダへの権限がない、読み取り専用、他プロセスで占有 | 適切なユーザーで実行する、ACLを確認する、ファイルを正しく閉じる |
|
一般的なI/Oエラー、ファイルが占有、ディスク不足 | try-catch を使い、ファイルを長時間開きっぱなしにしない |
|
パスやファイル名が長すぎる | パスを短くする、相対パスを使う |
|
不正なパス形式 | パス文字列に禁止文字がないかチェックする |
ファイルエラー時にどう動くかのフローチャート
flowchart TD
A[ファイル操作] --> B{例外が投げられたか?}
B -- いいえ --> C[操作は成功した]
B -- はい --> D{例外の種類は?}
D -- FileNotFound --> E[ユーザーに正しいファイルを指定させるか作成する]
D -- DirectoryNotFound --> F[不足しているディレクトリを作成する]
D -- UnauthorizedAccess --> G[必要な権限で再起動するようユーザーに促す]
D -- IOException --> H[誰がファイルを使っているか、ディスクを確認する]
D -- PathTooLong --> I[パスを短くする]
D -- NotSupported --> J[パスの形式を確認する]
D -- Other --> K[メッセージを表示してログを確認する]
8. 実アプリで典型的な例外の見え方
学習プロジェクトを例にしよう。ユーザーのメモをファイルに保存して、それを読み返す小さなプログラムがあるとする。
string notesPath = "notes.txt";
Console.Write("メモを入力してください: ");
string note = Console.ReadLine();
try
{
// メモを保存
using var writer = new StreamWriter(notesPath, true, Encoding.UTF8);
writer.WriteLine(note);
// すべてのメモを読み取る
Console.WriteLine("あなたのメモ:");
using var reader = new StreamReader(notesPath, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
Console.WriteLine("メモファイルが見つかりません。手動で作成してみてください。");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("メモファイルへのパスが正しくありません。フォルダが存在するか確認してください。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ファイルの読み書き権限がありません。管理者としてプログラムを実行してください。");
}
catch (IOException ex)
{
Console.WriteLine("入出力エラーが発生しました: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラー: " + ex.Message);
}
この例では、どのアプリでもあり得る典型的な流れを示している:データをファイルに保存してから読み込む。各 catch ブロックは特定のエラークラスを処理しており、ファイルやフォルダの欠如、権限問題、一般的なI/Oエラーまで扱っている。こうすることでプログラムは即座に落ちず、ユーザーに何が起きたかと対応方法を伝えられる。これがアプリをより信頼できて親切にするアプローチだ。
GO TO FULL VERSION