今回は「C#におけるファイル操作」と「リソース管理」について解説します。

設定ファイルの読み込み、ログの記録、データの永続化など、ファイル操作は実際のアプリ開発では必須のスキルです。

また、ファイル操作では「使い終わったらきちんと閉じる」というリソース管理が欠かせません。

本記事では以下の方に役立つ内容となっています。

  • ファイル操作の基本を理解したい初心者の方
  • リソース管理(Dispose、using)の概念を学びたい方
  • データの永続化方法を学びたい方

ファイル操作については、以下のレストランメニュー生成アプリでも扱ってきましたね。

C#入門編(18)NuGetパッケージの使い方 ~CSVファイルを読み込む~【Visual Studio+nuget】 実践的なアプリ開発では、他人が作った既存部品をいかにうまく使うかがキモになります。 だいたいのアプリ開発は以下のイメージです。 ...

今回あらためて、実務でも使うことの多いテキストファイルを中心に、ファイル操作の仕組みや、便利な標準ライブラリの使い方について、説明をします。

プロ太

ファイル操作はどんなアプリでも必要になる基本スキルです。リソース管理の考え方も含めて、一緒に理解を深めていきましょう!

動画も作成しています。

ファイル入出力の基本

ストリーム

C#のファイル入出力は「ストリーム(Stream)」という概念に基づいています。

ストリームとは、データの流れを表す抽象的な概念で、ファイルからデータを読み込んだり、ファイルへデータを書き込んだりする際の「通り道」のようなものです。

アプリ → [ストリーム] → ファイル(書き込み)
ファイル → [ストリーム] → アプリ(読み込み)

リソース管理の重要性

ファイル操作で最も重要なのは「使い終わったら必ず閉じる」ということです。

ファイルを開くと、OSがそのファイルへのアクセス権を確保します。これを「ファイルハンドル」と呼びます。ファイルを閉じないと、以下のような問題が発生します。

  • 他のプログラムがファイルにアクセスできない
    開いたままのファイルは、他のアプリから編集・削除できないことがある
  • リソースの枯渇
    OSが管理できるファイルハンドルには上限がある
  • データの損失
    書き込み内容がファイルに反映されないまま残る可能性がある
プロ美

ファイルを開きっぱなしにするとそんなに問題があるんだ!

プロ太

はい。だからC#ではusingステートメントを使って、確実にファイルを閉じる仕組みがあります。これは後ほど詳しく解説します。

主要なクラスと用途

C#にはファイル操作のための様々なクラスが用意されています。ここでは主要なものを一覧で紹介します。

カテゴリクラス用途
ストリーム操作FileStream低レベルなファイル操作
BinaryReader/BinaryWriterバイナリの読み書き
StreamReader/StreamWriterテキストの読み書き
File操作File一括読み書き(静的メソッド)
FileInfoファイル情報取得・操作
パス操作Pathパス文字列の操作
ディレクトリ操作Directoryディレクトリ操作(静的メソッド)
DirectoryInfoディレクトリ情報取得・操作

今回は表の中から、以下のものを解説します。

  • StreamReader / StreamWriter
  • Fileクラス(ReadAllTextWriteAllTextExistsなど)
  • Pathクラス(パス操作)
プロ太

最初にリソース管理の方法、基本となるStreamReader/Writerの使い方を紹介します。

次に、より高レベルなFileクラスや、あわせてよくつかうPathについての説明をします。

usingとDispose ~リソースを確実に解放する~

ファイル操作を含めた様々なリソースの管理において必要となるIDisposableインターフェイスや、usingステートメントの使い方について説明します。

IDisposableインターフェイス

C#では、ファイルやデータベース接続など「使い終わったら解放が必要なリソース」を扱うクラスは、IDisposableインターフェイスを実装しています。

public interface IDisposable
{
    void Dispose();
}

Dispose()メソッドを呼び出すことで、リソースを解放できます。

この後紹介するStreamReaderやStreamWriter、そしてFileStreamなど、ファイル操作に関わるクラスの多くがIDisposableを実装しています。

例外が発生した場合にも確実にDisposeを行うには、以下のような書き方が必要です。

var resource = new SomeResource(); // リソースを確保
try
{
    // ここでリソースを使った処理
}
finally
{
    // 例外が発生しても確実に解放
    resource.Dispose();
}

しかし、毎回このように書くのは面倒ですし、書き忘れの原因にもなります。そこでC#では、より簡潔に書けるusingステートメントが用意されています。

usingステートメントの書き方

従来の書き方(C# 7以前)は以下です。

using (var resource = new SomeResource())
{
    // リソースを使った処理
}// ここで自動的にresource.Dispose()が呼ばれる

usingブロックを抜けると、自動的にDispose()が呼ばれます。例外が発生した場合でも確実に呼ばれるため、安全です。

C# 8.0以降では、より簡潔な書き方ができます。

public static void Main(string[] args)
{
    using var resource = new SomeResource();
    // リソースを使った処理
    // スコープの終わりで自動的にDispose()が呼ばれる
}

変数のスコープが終わるタイミングでDispose()が呼ばれます。

usingを使わないとどうなるか

usingを使わないと、例外が発生した場合にDispose()が呼ばれず、リソースが解放されないままになってしまいます。

// 推奨されない書き方
var resource = new SomeResource();
resource.DoSomething();
// ここで例外が発生すると...
resource.Dispose(); // ← この行が実行されない!
プロ太

try-catch-finallyを使う方法もありますが、煩雑ですし忘れがちです。

なので、IDisposableを実装したクラスを使うときは、(スコープ内で完結する場合には、)必ずusingを使うようにしましょう!

ガーベジコレクション(GC)はメモリ回収のみでリソース解放はしません。

ファイナライザによるDispose()呼び出しを持つクラスもありますが、実行タイミングは不定です。確実な解放にはusingを使いましょう。

ストリームによるファイルの読み書き

ファイル操作の基本であるストリームについて簡単に紹介します。

StreamReaderで読み込み

StreamReaderの例は以下になります。

using System.Text;

string filePath = "sample.txt";

// (1)1行ずつ読み込む
using StreamReader reader1 = new StreamReader(filePath);
string line;
while ((line = reader1.ReadLine()) != null)
{
    Console.WriteLine(line);
}

// (2)ファイル全体を読み込む
using StreamReader reader2 = new StreamReader(filePath);
string content = reader2.ReadToEnd();
Console.WriteLine(content);

// (3)UTF-8(デフォルト)
using StreamReader reader3 = new StreamReader("utf8.txt", Encoding.UTF8);

// (4)Shift-JIS
// ↓.NET Coreでは標準外のエンコーディングを使うために登録が必要
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
using StreamReader reader4 = new StreamReader("shiftjis.txt", Encoding.GetEncoding("Shift_JIS"));
プロ太

最近はUTF-8が標準ですが、古いシステムとの連携ではShift-JISを使う場合もあります。

文字化けが発生したら、まずエンコーディングを確認しましょう。

StreamWriterで書き出し

StreamWriterの例は以下になります。

string filePath1 = "output.txt";

// (1)基本的な書き込み
using StreamWriter writer1 = new StreamWriter(filePath1);
writer1.WriteLine("1行目のテキスト");
writer1.WriteLine("2行目のテキスト");
writer1.WriteLine("3行目のテキスト");

// (2)追記モードでの書き込み
string filePath2 = "log.txt";
// append: true で追記モード
using StreamWriter writer2 = new StreamWriter(filePath2, append: true);
writer2.WriteLine($"{DateTime.Now}: アプリケーションが起動しました");
プロ太

ストリームはファイルを扱う上で基本となるものですが、実際には次に紹介するFileクラスを使うことが多いです。

Fileクラスによるシンプルなファイル操作

Fileクラスは、ファイル全体を一度に読み書きする便利な静的メソッドを提供します。

内部でSteamReader/StreamWriterを使用しており、Disposeも自動的に処理されるため、手軽に使えます。

ファイルの読み込み

読み込み方法はいくつかあります。

string filePath = "input.txt";

// (1)ファイル全体を文字列として読み込む
string content = File.ReadAllText(filePath);
Console.WriteLine(content);

// (2)行の配列として読み込む
string[] lines = File.ReadAllLines(filePath);

// (3)遅延読み込み(1行ずつ順番に読み込む)
foreach (string line in File.ReadLines(filePath))
{
    Console.WriteLine(line);
    // 途中で処理を終了しても、全体をメモリに読み込まない
}
プロ美

ReadAllLinesReadLinesって何が違うの?

プロ太

ReadAllLinesは全行を一度にメモリに読み込みます。ReadLinesは1行ずつ読み込むので、大きいファイルでもメモリを節約できます。

ファイルへの書き込み

書き込み方法もいくつかあります。

string filePath = "output.txt";

// (1)文字列をファイルに書き込む(既存ファイルは上書き)
string content = "これはテストです。\n2行目の内容。";
File.WriteAllText(filePath, content);

// (2)文字列の配列をファイルに書き込む(既存ファイルは上書き)
string[] lines = { "1行目", "2行目", "3行目" };
File.WriteAllLines(filePath, lines);

// (3)ファイルの末尾に追記
File.AppendAllText(filePath, $"{DateTime.Now}: 処理を実行しました\n");
プロ美

ログファイルみたいに、どんどん書き足していきたいときはAppendAllTextを使えばいいんだね。

Fileの各メソッドでは、エンコード指定を行うことなども可能です。

ファイルの存在確認と情報取得

ファイルを操作する前に、存在確認をしておくと安全です。

string filePath = "sample.txt";

// ファイルの存在確認
if (File.Exists(filePath))
{
    Console.WriteLine("ファイルが存在します");

    // ファイル情報を取得
    FileInfo fileInfo = new FileInfo(filePath);
    Console.WriteLine($"ファイルサイズ: {fileInfo.Length} バイト");
    Console.WriteLine($"作成日時: {fileInfo.CreationTime}");
    Console.WriteLine($"最終更新: {fileInfo.LastWriteTime}");
}
else
{
    Console.WriteLine("ファイルが存在しません");
}
プロ太

FileInfoは同じファイルの複数の属性を調べたいときなどに便利ですね。

ファイルのコピー・移動・削除

以下のようにファイルのコピー・移動・削除ができます。

// (1)ファイルのコピー(第3引数trueで上書き許可)
File.Copy("source.txt", "destination.txt", overwrite: true);

// (2)ファイルの移動(リネームにも使える)
File.Move("old_name.txt", "new_name.txt");

// (3)ファイルの削除
var tempFile = "temp.txt";
if (File.Exists(tempFile))
{
    File.Delete(tempFile);
}

非同期版について

Fileクラスには非同期版のメソッドも用意されています。UIアプリやWebアプリでファイルI/O中にUIがフリーズしないようにするために使います。

string content = await File.ReadAllTextAsync("sample.txt");
await File.WriteAllTextAsync("output.txt", content);

非同期処理については以下の記事も参考にしてください。

C#入門編(17)非同期処理(async, await, Task) ~複数の処理を並行して実行~ 今回は「非同期処理」について解説します。 非同期処理は、複数の処理を同時並行で効率的に実行するための仕組みです。 実は、こ...

例外処理

ファイル操作時には、「ファイルが存在しない(FileNotFoundException)」などの様々な例外が発生する可能性があるため、適切に例外処理も行うようにしましょう。

例外の扱い方については以下の記事も参考にしてください。

C#入門編(14)例外処理の基本(try,catch,throw)~アプリの「想定外」を防ぐ~ C#入門編では、基本的な文法やオブジェクト指向、インターフェイス、ジェネリック型などについてこれまで学んできました。 今回はC#...

Pathクラスによるパス操作

ファイル操作では、ファイルパスの組み立てや分解が頻繁に発生します。Pathクラスは、パス文字列を安全に操作するための静的メソッドを提供します。

パスの結合

Path.Combineを使うと、区切り文字を自動で補完してくれます。

// (1)基本的なパス結合
string folder = @"C:\Users\Documents";
string fileName = "report.txt";
string fullPath = Path.Combine(folder, fileName);
Console.WriteLine(fullPath);
// → "C:\Users\Documents\report.txt"

// (2)複数のパーツを結合
string basePath = Path.Combine("C:", "Users", "Documents", "Projects", "sample.txt");
Console.WriteLine(basePath);
// → "C:\Users\Documents\Projects\sample.txt"

// (3)区切り文字があってもなくても正しく処理される
string path1 = Path.Combine(@"C:\Folder\", "file.txt");
Console.WriteLine(path1); // → "C:\Folder\file.txt"
string path2 = Path.Combine(@"C:\Folder", "file.txt");
Console.WriteLine(path2); // → "C:\Folder\file.txt"
プロ美

Path.Combineを使えば、末尾に「\」があるかどうかを気にする必要がないんだね!

パスから情報を取得

ファイルパスから、ファイル名やディレクトリ名、拡張子などを取り出せます。

string filePath = @"C:\Users\Documents\report.txt";

// (1)ファイル名を取得(拡張子あり)
string fileName = Path.GetFileName(filePath);
Console.WriteLine(fileName);
// → "report.txt"

// (2)ファイル名を取得(拡張子なし)
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
Console.WriteLine(fileNameWithoutExt);
// → "report"

// (3)拡張子を取得
string extension = Path.GetExtension(filePath);
Console.WriteLine(extension);
// → ".txt"

// (4)ディレクトリ部分を取得
string directory = Path.GetDirectoryName(filePath);
Console.WriteLine(directory);
// → "C:\Users\Documents"

拡張子の変更

Path.ChangeExtensionを使うと、ファイルの拡張子を変更したパス文字列を取得できます。

string originalPath = @"C:\Users\Documents\report.txt";

// (1)拡張子を変更
string pdfPath = Path.ChangeExtension(originalPath, ".pdf");
Console.WriteLine(pdfPath);
// → "C:\Users\Documents\report.pdf"

// (2)実用例:ログファイルのバックアップ名を生成
string logFile = "app.log";
string backupFile = Path.ChangeExtension(logFile, ".log.bak");
Console.WriteLine(backupFile);
// → "app.log.bak"
プロ太

Path.ChangeExtensionは文字列操作のみを行います。実際のファイル名を変更するには、File.Moveと組み合わせて使いましょう。

絶対パスと相対パス

パスには「絶対パス」と「相対パス」の2種類があります。Pathクラスを使って、これらを判定したり変換したりできます。

// (1)絶対パスかどうかを判定
string absolutePath = @"C:\Users\Documents\file.txt";
string relativePath = @"Documents\file.txt";
Console.WriteLine(Path.IsPathFullyQualified(absolutePath)); // → True
Console.WriteLine(Path.IsPathFullyQualified(relativePath)); // → False

// (2)相対パスを絶対パスに変換
string relative = "data/sample.txt";
string absolute = Path.GetFullPath(relative);
Console.WriteLine(absolute);
// → 現在の作業ディレクトリを基準にした絶対パス
// 例: "C:\Projects\MyApp\data\sample.txt"

// (3)基準となる作業ディレクトリの確認
string currentDir = Directory.GetCurrentDirectory();
Console.WriteLine(currentDir);

// (4)実行ファイルのあるディレクトリを取得
string exeDir = AppContext.BaseDirectory;
string configPath = Path.Combine(exeDir, "config", "settings.json");
Console.WriteLine(configPath);
プロ太

作業ディレクトリはアプリの起動方法によって変わることがあります。

設定ファイルなど確実に読み込みたい場合はAppContext.BaseDirectoryを使うのが安全です。

まとめ

今回はC#におけるファイル操作リソース管理について解説しました。

usingステートメントを使えば、例外が発生しても確実にDispose()が呼ばれるため、安全にリソースを解放できます。

ファイル操作の方法は大きく2つあります。StreamReader/StreamWriterストリームベースで細かい制御が可能です。

一方、Fileクラスの静的メソッド(ReadAllTextWriteAllTextなど)は内部でDisposeも処理してくれるため、シンプルな操作に適しています。

また、パス操作にはPathクラスを活用しましょう。パス結合、拡張子の取り出し、絶対・相対パスの変換など便利なメソッドが揃っています。

ファイル操作はあらゆるアプリで必要となる基本スキルです。usingによるリソース管理を習慣づけて、安全なコードを書いていきましょう。

次回は、C#における設定ファイルの扱い方・読み込み方法について解説予定です。

プロ太

引き続き、C#について一緒に学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語