【C#】ファイル読み書き完全ガイド|StreamReader・StreamWriter・Fileクラス・FileStream・非同期・Path操作まで

C# のファイル操作には StreamReaderStreamWriter だけでなく、File クラスの便利なショートカット・バイナリを扱う FileStream・ファイルのメタ情報を操作する FileInfo/DirectoryInfo・パスを安全に組み立てる Path クラスなど、用途に応じた多彩な API があります。

本記事ではファイル読み書きに必要な知識を体系的に解説し、ログ書き込みや CSV 読み込みの実践例も紹介します。

スポンサーリンク

ファイル操作クラスの全体マップ

クラス 用途 代表的なシナリオ
File 静的メソッドで手軽に読み書き テキストファイルの一括読み書き・コピー・削除など
StreamReader テキストの逐次読み込み 大きなファイルを行ごとに処理・エンコーディング制御
StreamWriter テキストの逐次書き込み ログ書き込み・大量行の出力
FileStream バイトレベルの読み書き バイナリファイル・ランダムアクセス・Seek
FileInfo ファイルの情報・操作 存在確認・サイズ・作成日時・コピー・削除
DirectoryInfo ディレクトリの情報・操作 サブフォルダ一覧・ファイル一覧・作成・削除
Path パス文字列の操作 パス結合・拡張子取得・ファイル名取得(OS非依存)

File クラス:手軽な一括読み書き

ファイル全体を一度に読み書きするだけであれば、File クラスの静的メソッドが最もシンプルです。StreamReader/StreamWriter を明示的に生成する必要がありません。

File クラスの主要メソッド
// ─── 読み込み ───────────────────────────────────
// ファイル全体を1つの文字列として読む
string all = File.ReadAllText("data.txt");

// ファイルを行ごとの配列として読む
string[] lines = File.ReadAllLines("data.txt");
foreach (string line in lines)
    Console.WriteLine(line);

// ファイルをバイト配列として読む(バイナリ)
byte[] bytes = File.ReadAllBytes("image.png");

// ─── 書き込み ────────────────────────────────────
// 文字列を書き込む(ファイルがなければ作成、あれば上書き)
File.WriteAllText("output.txt", "Hello, World!");

// 複数行を一括書き込み
string[] rows = { "行1", "行2", "行3" };
File.WriteAllLines("output.txt", rows);

// ─── 追記 ────────────────────────────────────────
File.AppendAllText("log.txt", $"[{DateTime.Now}] 処理完了
");

// ─── コピー・移動・削除 ─────────────────────────
File.Copy("source.txt", "dest.txt", overwrite: true);
File.Move("old.txt", "new.txt", overwrite: true); // overwrite は .NET Core 3.0+
File.Delete("temp.txt");

// ─── 存在確認 ────────────────────────────────────
if (File.Exists("config.json"))
{
    string config = File.ReadAllText("config.json");
}
小〜中程度のファイル(数十 MB 以下)の一括読み書きには File クラスが最適です。数百 MB を超える巨大ファイルや、行ごとの逐次処理が必要な場合は StreamReader を使ってメモリ使用量を抑えましょう。

StreamReader:テキストの逐次読み込み

基本的な読み込みパターン

StreamReader の読み込みパターン
// ─── ReadLine: 1行ずつ読み込む(大ファイルに最適)───
using var reader = new StreamReader("large.txt");
string? line;
int lineNumber = 0;
while ((line = reader.ReadLine()) is not null)
{
    lineNumber++;
    Console.WriteLine($"{lineNumber,4}: {line}");
}

// ─── ReadToEnd: ファイル全体を文字列で取得 ────────
using var reader2 = new StreamReader("small.txt");
string content = reader2.ReadToEnd();

// ─── ReadToEndAsync: 非同期で全体を読む ──────────
using var reader3 = new StreamReader("data.txt");
string text = await reader3.ReadToEndAsync();

// ─── Peek: 次の文字を確認(消費しない)────────────
using var reader4 = new StreamReader("data.txt");
while (reader4.Peek() >= 0) // -1 になると終端
{
    int ch = reader4.Read(); // 1文字ずつ読む
    Console.Write((char)ch);
}

エンコーディングの指定(文字化け対策)

エンコーディングを指定して読み込む
// BOM なし UTF-8(最も一般的。Linux/macOS 標準)
using var r1 = new StreamReader("utf8.txt", System.Text.Encoding.UTF8);

// Shift-JIS(古い Windows ファイルに多い)
var sjis = System.Text.Encoding.GetEncoding("shift_jis");
using var r2 = new StreamReader("sjis.txt", sjis);

// BOM を自動検出して読み込む(デフォルト動作)
using var r3 = new StreamReader("auto.txt", detectEncodingFromByteOrderMarks: true);

// エンコーディング確認
Console.WriteLine(r3.CurrentEncoding.EncodingName);
new StreamReader("file.txt") のデフォルトは BOM を自動検出し、BOM がなければ UTF-8 として扱います。しかし Shift-JIS のファイルを UTF-8 として読むと文字化けします。既知のエンコーディングは必ず明示的に指定しましょう。

StreamWriter:テキストの逐次書き込み

StreamWriter の書き込みパターン
// ─── 上書きモード(デフォルト)────────────────────
using var writer = new StreamWriter("output.txt");
writer.WriteLine("1行目");
writer.WriteLine("2行目");
writer.Write("改行なし"); // WriteLine と違い改行を付けない

// ─── 追記モード(第2引数 true)──────────────────
using var appender = new StreamWriter("log.txt", append: true);
appender.WriteLine($"[{DateTime.Now:HH:mm:ss}] ログエントリ");

// ─── エンコーディング指定 ────────────────────────
// BOM なし UTF-8 で書き込む(UTF8NoBOM を明示)
var utf8NoBom = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
using var w2 = new StreamWriter("output_utf8nobom.txt", false, utf8NoBom);
w2.WriteLine("BOM なし UTF-8");

// BOM あり UTF-8(旧来の Windows 環境向け)
using var w3 = new StreamWriter("output_utf8bom.txt", false, System.Text.Encoding.UTF8);
w3.WriteLine("BOM あり UTF-8");

// ─── AutoFlush ───────────────────────────────────
using var logWriter = new StreamWriter("realtime.log", append: true)
{
    AutoFlush = true // 書き込みのたびに即座にフラッシュ(リアルタイムログ向け)
};
logWriter.WriteLine("リアルタイムで書き込まれる");
BOM(Byte Order Mark)について
System.Text.Encoding.UTF8(= Encoding.UTF8)は BOM あり UTF-8 エンコーダーを返します。Linux/macOS や多くの Web API・ツールは BOM なしを前提とするため、StreamWriter に渡すエンコーディングには new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) を明示すると安全です。なお .NET 5 以降の StreamWriter デフォルト(引数なし)は BOM なし UTF-8 になっています。

FileStream:バイナリファイルとランダムアクセス

FileStream はバイト単位でファイルを操作します。画像・音声・バイナリプロトコルなど、テキスト以外のファイルや、ファイルの途中から読み書きするランダムアクセスに使います。

FileStream の基本とバイナリ読み書き
// ─── バイナリファイルのコピー ──────────────────────
using var input  = new FileStream("source.bin", FileMode.Open,   FileAccess.Read);
using var output = new FileStream("dest.bin",   FileMode.Create, FileAccess.Write);

byte[] buffer = new byte[4096]; // 4KB バッファ
int bytesRead;
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
    output.Write(buffer, 0, bytesRead);

// ─── Seek:任意の位置から読む(ランダムアクセス)────
using var fs = new FileStream("data.bin", FileMode.Open);
fs.Seek(100, SeekOrigin.Begin);   // 先頭から100バイト目へ
fs.Seek(-10, SeekOrigin.End);     // 末尾から10バイト前へ
fs.Seek(20,  SeekOrigin.Current); // 現在位置から20バイト先へ

byte[] chunk = new byte[16];
fs.Read(chunk, 0, chunk.Length);

// ─── BinaryReader / BinaryWriter で型付き読み書き ──
using var bw = new BinaryWriter(new FileStream("typed.bin", FileMode.Create));
bw.Write(42);          // int (4 bytes)
bw.Write(3.14f);       // float (4 bytes)
bw.Write("Hello");     // string (length-prefixed)

using var br = new BinaryReader(new FileStream("typed.bin", FileMode.Open));
int   intVal    = br.ReadInt32();   // 42
float floatVal  = br.ReadSingle();  // 3.14
string strVal   = br.ReadString();  // "Hello"
FileMode 動作 主な用途
FileMode.Open 既存ファイルを開く(存在しなければ例外) 読み込み専用
FileMode.OpenOrCreate 存在すれば開き、なければ作成 読み書き兼用
FileMode.Create 新規作成(既存ファイルは上書き) 書き込み専用
FileMode.CreateNew 新規作成(既存ファイルがあれば例外) 重複防止
FileMode.Append 末尾から追記(なければ作成) ログ追記
FileMode.Truncate 既存ファイルを開き内容を消去 ファイルをクリアして書き直す

FileInfo と DirectoryInfo:ファイルとディレクトリのメタ情報

FileInfo の活用
\
var fi = new FileInfo("document.txt");

// ─── ファイルの情報取得 ────────────────────────────
Console.WriteLine(fi.Exists);             // 存在するか
Console.WriteLine(fi.Length);            // バイト数
Console.WriteLine(fi.CreationTime);      // 作成日時
Console.WriteLine(fi.LastWriteTime);     // 最終更新日時
Console.WriteLine(fi.Extension);        // ".txt"
Console.WriteLine(fi.Name);             // "document.txt"
Console.WriteLine(fi.DirectoryName);    // "C:\data" など

// ─── ファイルの操作 ────────────────────────────────
fi.CopyTo("backup.txt", overwrite: true); // コピー
fi.MoveTo("archive/document.txt");        // 移動
fi.Delete();                              // 削除
fi.Attributes = FileAttributes.ReadOnly; // 読み取り専用に変更

// ─── StreamReader/Writer の取得 ────────────────────
using StreamReader reader = fi.OpenText();
string text = reader.ReadToEnd();

using StreamWriter writer = fi.AppendText(); // 追記用
writer.WriteLine("追記テキスト");
DirectoryInfo の活用
\
var di = new DirectoryInfo(@"C:\data");

// ─── ディレクトリの操作 ────────────────────────────
di.Create();                             // ディレクトリ作成
Console.WriteLine(di.Exists);           // 存在確認
Console.WriteLine(di.FullName);         // フルパス
Console.WriteLine(di.Parent?.FullName); // 親ディレクトリ

// ─── ファイル・サブディレクトリの一覧取得 ──────────
foreach (FileInfo file in di.GetFiles("*.txt"))
    Console.WriteLine($"{file.Name} ({file.Length} bytes)");

// サブディレクトリも含めて検索(再帰)
foreach (FileInfo file in di.GetFiles("*.log", SearchOption.AllDirectories))
    Console.WriteLine(file.FullName);

// ─── Directory 静的クラスも便利 ───────────────────
string[] csvFiles = Directory.GetFiles(@"C:\data", "*.csv", SearchOption.AllDirectories);
bool exists = Directory.Exists(@"C:\data\logs");
Directory.CreateDirectory(@"C:\data\logs\2024"); // 親ディレクトリがなくても作成

// ─── 列挙(大量ファイルはこちらが効率的)─────────
foreach (string path in Directory.EnumerateFiles(@"C:\data", "*.txt"))
    Console.WriteLine(path);
Directory.GetFiles() はすべてのパスをメモリに格納してから返しますが、Directory.EnumerateFiles() は1件ずつ返すため、数万件のファイルがあるディレクトリでもメモリを節約できます。

Path クラス:パス文字列の安全な操作

パスを文字列結合("C:\" + "data\" + "file.txt")で組み立てると、OS によってセパレータが異なるためバグになります。Path クラスを使うと OS 非依存で安全にパスを扱えます。

Path クラスの主要メソッド
\
string filePath = @"C:\data\logs\app.log";

// ─── パスの分解 ────────────────────────────────────
Console.WriteLine(Path.GetFileName(filePath));          // "app.log"
Console.WriteLine(Path.GetFileNameWithoutExtension(filePath)); // "app"
Console.WriteLine(Path.GetExtension(filePath));         // ".log"
Console.WriteLine(Path.GetDirectoryName(filePath));     // "C:\data\logs"
Console.WriteLine(Path.GetPathRoot(filePath));          // "C:\"

// ─── パスの結合(OS のセパレータを自動適用)──────
string combined = Path.Combine("C:\\data", "logs", "2024", "app.log");
// Windows: "C:\data\logs\2024\app.log"
// Linux:   "C:/data/logs/2024/app.log"
Console.WriteLine(combined);

// ─── 一時ファイル・一時フォルダ ──────────────────
string tempFile = Path.GetTempFileName();  // 一時ファイルを作成してパスを返す
string tempDir  = Path.GetTempPath();      // OS の一時フォルダパス
string randomName = Path.GetRandomFileName(); // ランダムなファイル名(作成はしない)

// ─── パスの確認 ────────────────────────────────────
bool isRooted = Path.IsPathRooted(@"C:\data"); // true(絶対パス)
string full   = Path.GetFullPath("../relative"); // 絶対パスに変換

// ─── 拡張子の変更 ─────────────────────────────────
string newPath = Path.ChangeExtension("report.txt", ".pdf"); // "report.pdf"
Windows でパスを文字列で直接書くと "
"
がエスケープシーケンス(改行)として解釈されるケースがあります。バックスラッシュを含むパスは @"C:\data"(逐語的文字列)か Path.Combine を使いましょう。

非同期ファイル操作

I/O 処理はスレッドをブロックしやすいため、async/await を使った非同期 API を活用すると、UI やサーバーのレスポンス性能が向上します。

非同期ファイル操作(File クラスと StreamReader/Writer)
// ─── File クラスの非同期メソッド ──────────────────
string text = await File.ReadAllTextAsync("data.txt");
string[] lines = await File.ReadAllLinesAsync("data.txt");
await File.WriteAllTextAsync("output.txt", "非同期で書き込み");
await File.AppendAllTextAsync("log.txt", $"[{DateTime.Now}] イベント
");

// ─── StreamReader の非同期読み込み ────────────────
async Task<List<string>> ReadLinesAsync(string path)
{
    var result = new List<string>();
    using var reader = new StreamReader(path);
    string? line;
    while ((line = await reader.ReadLineAsync()) is not null)
        result.Add(line);
    return result;
}

// ─── StreamWriter の非同期書き込み ────────────────
async Task WriteLogAsync(string logPath, IEnumerable<string> entries)
{
    using var writer = new StreamWriter(logPath, append: true);
    foreach (var entry in entries)
        await writer.WriteLineAsync(entry);
}

// ─── CancellationToken 対応 ────────────────────────
async Task ReadWithCancelAsync(string path, CancellationToken ct)
{
    // .NET 6 以降: ReadAllTextAsync は CancellationToken を受け取れる
    string content = await File.ReadAllTextAsync(path, ct);
    Console.WriteLine(content);
}

実践例

ログファイルへの書き込み

ロールオーバー付きログ書き込みクラス
class SimpleLogger : IDisposable
{
    private readonly StreamWriter _writer;
    private bool _disposed;

    public SimpleLogger(string logDirectory)
    {
        Directory.CreateDirectory(logDirectory);
        string fileName = $"app_{DateTime.Today:yyyyMMdd}.log";
        string path = Path.Combine(logDirectory, fileName);
        _writer = new StreamWriter(path, append: true, System.Text.Encoding.UTF8)
        {
            AutoFlush = true
        };
    }

    public void Info(string message)  => Write("INFO ", message);
    public void Error(string message) => Write("ERROR", message);
    public void Warn(string message)  => Write("WARN ", message);

    private void Write(string level, string message)
        => _writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}");

    public void Dispose()
    {
        if (!_disposed) { _writer.Dispose(); _disposed = true; }
    }
}

// 使い方
using var logger = new SimpleLogger("logs");
logger.Info("アプリケーション起動");
logger.Warn("設定ファイルが見つかりません。デフォルト値を使用します");
logger.Error("データベース接続に失敗しました");

CSV ファイルの読み込み

CSV ファイルを行ごとに処理する
// 大きな CSV ファイルを StreamReader で1行ずつ処理(メモリ効率が良い)
async Task<List<Product>> ReadCsvAsync(string path)
{
    var products = new List<Product>();
    using var reader = new StreamReader(path, System.Text.Encoding.UTF8);

    // ヘッダー行をスキップ
    string? header = await reader.ReadLineAsync();
    if (header is null) return products;

    string? line;
    int lineNumber = 1;
    while ((line = await reader.ReadLineAsync()) is not null)
    {
        lineNumber++;
        if (string.IsNullOrWhiteSpace(line)) continue;

        // カンマ区切りでパース(引用符は考慮しない簡易版)
        string[] cols = line.Split(',');
        if (cols.Length < 3)
        {
            Console.WriteLine($"警告: {lineNumber}行目のカラム数が不足しています");
            continue;
        }

        if (!decimal.TryParse(cols[2].Trim(), out decimal price))
        {
            Console.WriteLine($"警告: {lineNumber}行目の価格が不正です: {cols[2]}");
            continue;
        }

        products.Add(new Product
        {
            Id    = cols[0].Trim(),
            Name  = cols[1].Trim(),
            Price = price
        });
    }
    return products;
}

record Product { public string Id = ""; public string Name = ""; public decimal Price; }

よくある落とし穴と注意点

using を使わずにリソースを解放し忘れる

StreamReaderStreamWriterFileStreamIDisposable を実装しています。using で囲まないと、例外が発生したときにファイルハンドルが解放されず、「別のプロセスが使用中」エラーの原因になります。必ず using ステートメントまたは using var 宣言を使いましょう。

File.ReadAllText でメモリ不足になる

数百 MB を超えるファイルを File.ReadAllText で読むと、ファイル全体がメモリに載るため OutOfMemoryException が発生する場合があります。大きなファイルは StreamReader.ReadLine() で1行ずつ処理し、メモリ使用量を抑えましょう。

BOM の有無で文字化けする

Windows の「メモ帳」で保存した UTF-8 ファイルは BOM(EF BB BF)が付きます。new StreamReader(path) は BOM を自動検出して除去しますが、File.ReadAllBytesFileStream で直接バイトを読むと BOM がデータとして残ります。また、Linux/macOS で作成した UTF-8 ファイルを BOM あり UTF-8 として書き込むと、先頭に不要な文字が入ることがあります。書き込みには new UTF8Encoding(false)(BOM なし)を明示するのが安全です。

パスをハードコーディングする

"C:\data\file.txt" のように絶対パスをコードに直書きすると、環境が変わったときに動かなくなります。実行ファイルからの相対パスには AppContext.BaseDirectoryEnvironment.CurrentDirectory を起点に Path.Combine で組み立てましょう。

ファイルロックに気づかずエラーが出る

別のプロセスが開いているファイルを書き込みモードで開こうとすると IOException(「別のプロセスがファイルを使用中」)が発生します。読み取り専用で開く場合は new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) のように FileShare.ReadWrite を指定すると、他プロセスが書き込み中でも読み取れます。

よくある質問

QStreamReader と File.ReadAllText はどちらを使うべきですか?
Aファイルサイズが小さい(目安: 数十 MB 以下)かつ全体を一度に使うなら File.ReadAllText が簡潔です。大きなファイルや行ごとに処理したい場合は StreamReader.ReadLine() でメモリを節約します。非同期で扱いたい場合はどちらも Async 版があります。
Qテキストファイルのエンコーディングを自動検出できますか?
A完全な自動検出は困難です。StreamReader は BOM(バイト順マーク)があれば自動検出できますが、BOM なしのファイルは UTF-8 と仮定します。Shift-JIS など BOM を付けないエンコーディングは判定できないため、アプリケーションの要件としてエンコーディングを固定するか、設定ファイルで指定させる設計にするのが現実的です。
QPath.Combine と文字列結合の違いは何ですか?
APath.Combine("C:\data", "file.txt") は OS のパス区切り文字(Windows は \、Linux/macOS は /)を自動適用し、重複するセパレータも除去します。文字列結合("C:\data" + "\" + "file.txt")は手動管理が必要でクロスプラットフォーム対応が難しくなります。常に Path.Combine を使いましょう。
Q非同期でファイルを書き込む場合の注意点は?
Aawait writer.WriteLineAsync() を使う場合、AutoFlush = true を設定しないと内部バッファが溜まって即座にファイルに書かれない場合があります。using ブロックを抜けた時点で自動的に Flush と Dispose が呼ばれるため、通常はループ後に await writer.FlushAsync() を明示するか AutoFlush = true を設定しましょう。
Qディレクトリを再帰的に削除するには?
ADirectory.Delete(path, recursive: true) で中身ごとディレクトリを削除できます。recursive: false(デフォルト)では空でないディレクトリの削除は IOException になります。本番環境では誤って重要なディレクトリを削除しないよう、パスを十分に確認してから実行してください。

まとめ

クラス・API 主な用途 ポイント
File テキストの一括読み書き・コピー・削除 手軽。大ファイルは非推奨
StreamReader テキストの逐次読み込み 大ファイルも安全。エンコーディング明示推奨
StreamWriter テキストの逐次書き込み・追記 AutoFlush / BOM なし UTF-8 に注意
FileStream バイナリ操作・ランダムアクセス FileMode / FileAccess / FileShare を指定
BinaryReader/Writer 型付きバイナリ読み書き int・float・string を簡単に読み書き
FileInfo ファイルのメタ情報取得・操作 Length / CreationTime / MoveTo / Delete
DirectoryInfo ディレクトリ操作・ファイル一覧 EnumerateFiles で大量ファイルも効率的
Path パス文字列の OS 非依存操作 Combine / GetFileName / GetExtension
非同期 Async 系 UI やサーバーのレスポンス維持 File.*Async / ReadLineAsync を使う

リソースの解放(using/IDisposable)の詳細はusing宣言とIDisposableの基本を、ファイル I/O の例外処理は例外処理完全ガイドを参照してください。