C# ファイルIO の正解 — StreamReader / File.ReadAllLines / File.ReadLines / using の使い分け

みなさんこんにちは!ヒロポンです!!

今回はC#業務SE現場でガチで踏みやすいやつ!!の話。

「CSV取り込みバッチを書いたら改行コードの違いで全行が1行に化けた」「Shift-JISの取引先データを読んだら文字化けで??????だらけ」「File.ReadAllLinesで1GBのログを読もうとしたらOutOfMemoryExceptionで落ちた」みたいなファイルIOの事故って、業務SEで一回はやらかしますよね??

俺も2社目くらいの流通系SIer時代に、取引先から来たShift-JISのCSVをFile.ReadAllText(path)(既定UTF-8)で読んで、全行?????で文字化けした事件をやらかしました。夕方の運用報告で「データが文字化けしてる」って報告で気づいて半日デバッガで追ってハマったやつ。原因は完全にエンコーディング指定漏れで、StreamReaderEncoding.GetEncoding("shift_jis")を渡すパターンに書き換えて解決。

C#でファイルを読むAPIは3系統+ usingの理解が要ります:

  • StreamReader(クラスベース、エンコーディング指定可、最も柔軟)
  • File.ReadAllLines(一発で全行配列、メモリ食う、小ファイル向き)
  • File.ReadLinesIEnumerableで遅延読み込み、大ファイル向き)
  • usingIDisposable自動解放、StreamReader系で必須)

この記事ではVS2019 / .NET Framework 4.7.2 / C# 7.3環境で、3系統の使い分けとエンコーディング・改行・FileShare・OOMの落とし穴を、コード6本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • 小ファイル(数KB〜数MB)→ File.ReadAllLines(コード短い・即配列)
  • 大ファイル(数十MB〜GB)→ File.ReadLines + foreach(メモリ節約・ストリーム処理)
  • エンコーディング指定したい時(Shift-JIS / BOM付き)→ StreamReader + using + Encoding指定
目次

定石1: StreamReader + using + Encoding指定

最も柔軟な書き方。エンコーディング指定したい業務系のCSV /取引先連携データはこれが本命:

// ✅定石1: StreamReader + usingでShift-JIS読み込み
using System.IO;
using System.Text;

var sjis = Encoding.GetEncoding("shift_jis");

using (var sr = new StreamReader(@"C:\input\trade-data.csv", sjis))
{
    string line;
    while ((line = sr.ReadLine())!= null)
    {
        // 1行ずつ処理(CSV分解→ DB INSERT等)
        ProcessOneRow(line);
    }
}
// ↑ブロック終了でsr.Dispose()がfinallyで確実に呼ばれる

ポイント:

  1. Encoding.GetEncoding("shift_jis")で日本語業務系の典型CSVに対応
  2. usingでIDisposable自動解放Close()書き忘れによるロック予防)
  3. ReadLine()で1行ずつ読む(メモリ節約)
  4. null判定でファイル末尾を検出(whileループの定石)

UTF-8 BOM付きならEncoding.UTF8、UTF-8 BOMなしならnew UTF8Encoding(false)を渡す。.NET FrameworkはShift-JIS標準対応ですが、.NET Core / .NET 5+ではSystem.Text.Encoding.CodePagesのNuGet追加が要るので注意っす。

定石2: File.ReadAllLines —小ファイル向きの一撃

ファイルサイズが小さい(数KB〜数MB)場合、コード短く書けるFile.ReadAllLinesが本命:

// ✅定石2: File.ReadAllLinesで一発配列化
using System.IO;
using System.Text;

string[] lines = File.ReadAllLines(@"C:\input\config.txt", Encoding.UTF8);

foreach (var line in lines)
{
    if (line.StartsWith("#"))continue;   //コメント行スキップ
    ParseConfig(line);
}

ポイント:

  1. 1行で全行配列化(コード量最小)
  2. エンコーディングは第2引数で指定可能
  3. 改行コード\r\n / \n / \r全部を行区切りで扱う(業務系で改行混在に強い)
  4. 配列なのでLength / LINQがそのまま効く

ただし欠点としてメモリにファイル全体をロードするので大ファイルだとOutOfMemoryExceptionが飛ぶ。1GBのログをReadAllLinesで読むと、配列+文字列オブジェクトのオーバーヘッドで4GB近いメモリを食う事故が起きます。ん?ファイルサイズ気にするとか面倒じゃない??って思うかもだけど、サイズが読めるファイルだけで使う判断軸を入れるだけで業務系のOOM事故はほぼ消えます。

定石3: File.ReadLines —大ファイルのストリーム読み込み

大ファイル(数十MB〜GB)の場合、File.ReadLinesIEnumerable<string>を返してもらって遅延読み込みするパターン:

// ✅定石3: File.ReadLinesで大ファイルをストリーム処理
using System.IO;
using System.Text;

// IEnumerable<string>なのでforeachで1行ずつ読む(メモリには1行分しか乗らない)
foreach (var line in File.ReadLines(@"C:\logs\app-2026-05.log", Encoding.UTF8))
{
    if (line.Contains("ERROR"))
    {
        ExtractErrorEntry(line);
    }
}

// LINQ連携も可能(遅延評価で全件メモリに乗らない)
var errorCount = File.ReadLines(@"C:\logs\app-2026-05.log", Encoding.UTF8)
    .Count(l => l.Contains("ERROR"));

ポイント:

  1. 遅延評価で1行ずつ読み込む(10GBファイルでもメモリ数MB)
  2. LINQメソッドチェーンが効くWhere / Count / Take等)
  3. 内部でStreamReaderを使ってるusing不要・自動Dispose)
  4. 改行コード混在に対応

業務系のログ集計・大量CSV取り込み・帳票出力で本命のパターン。前回の記事「DataReader vs DataAdapter」と同じ発想で、メモリに溜め込まずストリーム処理する判断軸っす。

定石4:性能比較— Stopwatch + GC.GetTotalMemory

File.ReadAllLinesFile.ReadLinesの体感差を実測すると、桁違いの違いが見えます:

// ✅定石4:性能比較(10万行×約100MBのログを想定)
const string path = @"C:\logs\big-app.log";

// ReadAllLines版(メモリに全部ロード)
GC.Collect(); GC.WaitForPendingFinalizers();
long memBefore = GC.GetTotalMemory(true);
var sw1 = Stopwatch.StartNew();

string[] all = File.ReadAllLines(path, Encoding.UTF8);
int errCnt1 = all.Count(l => l.Contains("ERROR"));

sw1.Stop();
long memAfter = GC.GetTotalMemory(false);
Console.WriteLine($"ReadAllLines: {sw1.ElapsedMilliseconds}ms / {(memAfter - memBefore)/ 1024 / 1024}MB / {errCnt1}件");

// ReadLines版(ストリーム読み込み)
GC.Collect(); GC.WaitForPendingFinalizers();
memBefore = GC.GetTotalMemory(true);
var sw2 = Stopwatch.StartNew();

int errCnt2 = File.ReadLines(path, Encoding.UTF8).Count(l => l.Contains("ERROR"));

sw2.Stop();
memAfter = GC.GetTotalMemory(false);
Console.WriteLine($"ReadLines   : {sw2.ElapsedMilliseconds}ms / {(memAfter - memBefore)/ 1024 / 1024}MB / {errCnt2}件");

俺の手元(VS2019 / .NET Framework 4.7.2 / x64 Release / 100MBログ・10万行)の体感差は、こんな感じ:

API 実行時間 メモリ消費
File.ReadAllLines 約850ms 約350MB
File.ReadLines 約720ms 約8MB

メモリ消費が桁違い(約44倍)なのが見える。ReadAllLinesは配列+文字列オブジェクトのヒープオーバーヘッドが大きいので、100MBのファイルで350MBのメモリを食う。1GBのログだと4GB近く食ってOutOfMemoryExceptionで落ちるケースもあるので、サイズが読めない業務系のファイル取り込みはReadLines寄せが安全っす。

定石5: FileShareで同時アクセスを扱う

ログファイルが別プロセスから書き込み中でも読みたい場合、FileShare.ReadWriteを指定する書き方:

// ✅定石5: FileShare.ReadWriteで同時アクセスを許可
using System.IO;
using System.Text;

using (var fs = new FileStream(@"C:\logs\writing.log",
                               FileMode.Open,
                               FileAccess.Read,
                               FileShare.ReadWrite))//別プロセスの読み書きも許可
using (var sr = new StreamReader(fs, Encoding.UTF8))
{
    string line;
    while ((line = sr.ReadLine())!= null)
    {
        ProcessLogLine(line);
    }
}

ポイント:

  • FileShare.None:排他(既定)。他プロセスは開けない
  • FileShare.Read:他プロセスが読みはOK、書きは不可
  • FileShare.ReadWrite:他プロセスの読み書き両方OK

業務系の「ログを書き込み中のままリアルタイム監視したい」ような用途で効くやつ。ただし、書き込み中の不完全な行が読めるリスクがあるので、業務系では「ログローテーション後に取り込む」設計の方が安全っす。

定石6:エンコーディング判定の実践— Shift-JISとBOM付きUTF-8の見分け

業務系で取引先から来るファイルは、エンコーディングが何かを送り主に聞くのが本筋ですが、確認できない時は最初の数バイトで判定する手もあります:

// ✅定石6: BOMの有無で簡易判定(業務系で雑なエンコーディング判定)
public static Encoding DetectEncoding(string path)
{
    var bytes = new byte[4];
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
    {
        fs.Read(bytes, 0, 4);
    }

    // UTF-8 BOM (EF BB BF)
    if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
        return Encoding.UTF8;

    // UTF-16 BOM (FF FE / FE FF)
    if (bytes[0] == 0xFF && bytes[1] == 0xFE)return Encoding.Unicode;
    if (bytes[0] == 0xFE && bytes[1] == 0xFF)return Encoding.BigEndianUnicode;

    // BOMなし→業務系の典型はShift-JIS(取引先Excel CSV出力等)
    return Encoding.GetEncoding("shift_jis");
}

//使う側
var enc = DetectEncoding(@"C:\input\trade.csv");
foreach (var line in File.ReadLines(path, enc))
{
    ProcessOneRow(line);
}

これは雑な判定で、BOMなしUTF-8をShift-JIS扱いにする可能性があります。業務系では送り主にエンコーディングを聞く運用が一番安全で、自前判定は最終手段に留めるのが鉄則っす。

ハマりポイント—実体験ベースの本番事故3点

1. Shift-JIS CSVをUTF-8で読んで全行文字化け(半日デバッガで追ってハマった)

取引先から来たCSVをFile.ReadAllText(path)(既定UTF-8)で読んで全行?????になった事件。夕方の運用報告で気づいて半日デバッガで追ってハマった末に、StreamReader + Encoding.GetEncoding("shift_jis")に書き換えて解決。それ以来、業務系チームで「取引先CSVはエンコーディング確認+ StreamReaderでEncoding指定」をルール化しました。

2. 1GBログをReadAllLinesで読んでOOM(30分溶かした)

監視ツールで1GBのアクセスログをFile.ReadAllLinesで読み込もうとして、OutOfMemoryExceptionで詰まった事件。30分溶かした末に、File.ReadLines + foreachのストリーム処理に書き換えてメモリ8MB程度に収まった。それ以来、ログ・CSVなど可変サイズのファイルは原則ReadLines寄せを業務系チーム規約にしました。

3. using漏れでファイルロック残留(数日プロファイラで追った)

StreamReaderusingで囲わずClose()も呼んでなくて、別プロセスが「他のプロセスが使用中」エラーで詰まる事件。数日プロファイラで追ってようやく気付いた。GCが走るまでOSレベルでファイルハンドルが解放されないので、業務系の連携バッチで間欠的に発生してた。usingで囲うパターンに統一して解決。ファイル系IDisposableは全部usingを業務系チーム規約に揃えました。

俺の現場メモ—業務系チームでのファイルIOルール

流通系SIer時代に、過去コードをgrep -rnE "File\.ReadAll|StreamReader|new FileStream" .でひっかけたら、90箇所近く出てきたんですよね。書き方がバラバラで、File.ReadAllTextでエンコーディング指定なし、StreamReaderusingなしで使う、File.ReadAllLinesで大ファイルを読んでOOMリスクある、全部入り。

んで、後輩と一緒に3行ルールにまとめた:

  1. エンコーディング指定したいファイルはStreamReader + using + Encoding(取引先CSV / Shift-JIS)
  2. 小ファイル専用でFile.ReadAllLines(サイズが読めない時は禁止)
  3. 大ファイル・サイズ不明はFile.ReadLines + foreach寄せ(メモリ事故予防)

このルール化で、ファイルIO周りの文字化け/ OOM /ロック残留事故が消えた。書き方を1パターンに揃えるだけで保守工数も事故率も両方下がるので、業務系チームには結構おすすめのルールっす。

C# 7.3 + .NET Framework 4.7.2のレガシー業務系って、ファイルIO APIは10年以上変わってないのに、書き方が現場ごとにバラバラなコードベースが本当に多い。書き方のアップデートだけで保守工数が下がる実例だと思ってます。

まとめ

状況 推奨API
小ファイル(数KB〜数MB)の単発読み込み File.ReadAllLines
大ファイル(数十MB〜GB) File.ReadLines + foreach
エンコーディング指定したい StreamReader + using + Encoding
別プロセス書き込み中の同時アクセス FileStream + FileShare.ReadWrite
エンコーディング判定 BOMチェックor送り主に確認
usingの役割 IDisposable自動解放(ファイルロック予防)
改行コード混在 標準API(ReadAllLines / ReadLines)に任せる
自前string.Split('\n') 禁止(\r残留で文字化け)

C#のファイルIO事故は、「サイズでAPIを選ぶ」「エンコーディング指定」「usingで囲う」の3点で9割消えます。File.ReadAllTextを裸で呼ぶ運用は事故の温床なので、業務系チームではサイズ・エンコーディング・usingの3要素を揃えるレビュー観点を入れておくのが安全っす。

よくある質問

Q1. File.ReadAllLinesFile.ReadLinesはどっちを使うべき?

A.ファイルサイズで分けます。小ファイル(数KB〜数MB)ならReadAllLines(一発で配列、コード短い)、大ファイル(数十MB〜GB)ならReadLinesIEnumerableで1行ずつ遅延読み込み、メモリ節約)。1GBのログをReadAllLinesで読むと数GBのメモリを食ってOutOfMemoryExceptionが飛びますが、ReadLinesなら数MBで済みます。サイズ不明な業務系ではReadLines + foreach寄せが安全です。

Q2. Shift-JISのCSVを読むと文字化けします、どうすれば?

A. StreamReaderにエンコーディングを明示してください。new StreamReader(path, Encoding.GetEncoding("shift_jis"))の形。.NET Frameworkは既定でShift-JISを扱えますが、.NET Core / .NET 5+ではSystem.Text.Encoding.CodePagesパッケージが必要です。BOM付きUTF-8ファイルはEncoding.UTF8でOKですが、BOMなしUTF-8をShift-JISで読むと派手に文字化けするので、業務系では取り込み前にファイルのエンコーディング確認を運用化するのが鉄則です。

Q3. usingを書き忘れたらどうなりますか?

A.ファイルがロックされ続けて、別プロセス・別スレッドからアクセスできなくなります。StreamReaderusingで囲わずClose()も呼ばないと、GCが走るまでOSレベルでファイルハンドルが解放されない。業務系で「他のプロセスが使用中」エラーが出る原因の典型です。StreamReader / StreamWriter / FileStreamは全部usingで囲うのが鉄則で、CLAUDE.mdの例外処理ルールと整合させると保守工数も下がります。

Q4.ファイルを別プロセスが開いている時にも読みたい場合は?

A. FileShare.ReadWriteを指定してFileStreamを開き、それをStreamReaderでラップします。new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)の形。ログファイルを別プロセスが書き込んでる最中に取り込みたい時に効きます。ただし、書き込み中の不完全な行が読めるリスクがあるので、業務系ではファイルロックを尊重する設計が無難です。

Q5.改行コードが\r\n / \n / \rで混在していて行数がズレるのは?

A. File.ReadAllLines / File.ReadLinesは3種類の改行コード全部を行区切りとして扱うので、混在していても正しく分割されます。問題が起きるのはstring.Split('\n')のように手書きで分割した時で、\rが行末に残って文字化けの原因になります。改行を含むファイルは標準API(ReadAllLines / ReadLines)に任せるのが安全で、自前splitは避けるのが鉄則です。

ここまででC#ファイルIOの主要API・性能・エンコーディング・FileShareは押さえた。using / IDisposable /メモリ性能の隣接トピックも貼っておきます。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次