C#のFile.ReadLinesの安全な使い方 - なぜストリームを閉じる必要があるのか

## **まえがき**
File.ReadLinesは **遅延評価（IEnumerable<string>）** を利用し、1行ずつストリームで処理する。
foreachで全行読み込めば自動でDisposeされる。
使い方を誤るとファイルが開きっぱなしになり、リソースリークを引き起こす可能性がある。

## **そもそも、なぜストリームを閉じる必要があるのか？**
C# に限らず、多くのプログラミング言語では**ファイルを開いたら、明示的に閉じることが推奨される**。理由は以下の通り：  

1. **ファイルハンドルの枯渇**  
   - OS には **一度に開けるファイルの数に上限** がある。（Windows では通常 `2048`、Linux では `ulimit -n` で確認できる）  
   - `File.ReadLines` は `StreamReader` を内部で使っているため、適切に閉じないとファイルハンドルが解放されない。
   - たとえば、長時間動作するアプリが `File.ReadLines` を繰り返し使い、ストリームを閉じないと、最終的に **新たなファイルが開けなくなる**。

2. **ロックが解除されない**  
   - `File.ReadLines` を使ってストリームが開いたままだと、Windows では **他のプロセスがそのファイルを編集・削除できなくなる**。
   - たとえば、Excel で CSV ファイルを開いた状態で編集できないのと同じ。
   - `ReadLines` を途中で止めた場合、**プログラムを終了するまでファイルがロックされたまま** になり、別のプロセスがアクセスできなくなる。

3. **メモリリーク（厳密にはハンドルリーク）**  
   - ガベージコレクション（GC）は**メモリの解放はするが、OS のファイルハンドルを解放しない**。
   - そのため、明示的に `Dispose` しないと、使われなくなった `StreamReader` のインスタンスがGCに回収されるまで、**無駄にOSリソースを消費し続ける**。

---

## **実際のリソースリークの例**
以下のようなコードがあるとする。

```csharp
void ProcessFiles()
{
    for (int i = 0; i < 1000; i++)
    {
        var lines = File.ReadLines("example.txt");
        foreach (var line in lines.Take(5)) // 最初の5行だけ取得
        {
            Console.WriteLine(line);
        }
    }
}
```

**問題点**：
- `File.ReadLines("example.txt")` は `StreamReader` を開くが、閉じられないまま `ProcessFiles()` のループを繰り返す。
- 1000回繰り返すうちに、**1000個のストリームが開きっぱなし** になり、最終的に OS から「ファイルを開けない」というエラー (`IOException: The process cannot access the file`) が発生する可能性がある。

---

## **解決策：`using` を使う**
明示的にストリームを閉じるためには、以下のように `GetEnumerator()` を `using` で囲む。

```csharp
void ProcessFiles()
{
    for (int i = 0; i < 1000; i++)
    {
        using (var lines = File.ReadLines("example.txt").GetEnumerator())
        {
            int count = 0;
            while (lines.MoveNext() && count < 5)
            {
                Console.WriteLine(lines.Current);
                count++;
            }
        } // ここで Dispose() が呼ばれるのでストリームが閉じる
    }
}
```

こうすることで、**ループごとにストリームが確実に閉じられる**ので、OSのファイルリソースが枯渇することはない。

---

## **「開きっぱなし」が問題になる具体例**
### **例①：別のプロセスがファイルを削除できない**
```csharp
var lines = File.ReadLines("example.txt");
foreach (var line in lines.Take(5)) 
{
    Console.WriteLine(line);
}

// 別のプロセス（例：エクスプローラーやバッチ処理）で `example.txt` を削除しようとすると失敗する
File.Delete("example.txt"); // IOException: ファイルが別のプロセスで使用中です
```

`File.ReadLines` の列挙が終わっていないため、**ファイルがロックされたままになっている**。

---

### **例②：ファイルを開きすぎて例外が発生**
```csharp
for (int i = 0; i < 10000; i++)
{
    var lines = File.ReadLines("example.txt"); // ストリームを開くが閉じない
    foreach (var line in lines.Take(1)) 
    {
        Console.WriteLine(line);
    }
}
```

しばらくすると、次のエラーが出る可能性がある。

```
System.IO.IOException: The process cannot access the file 'example.txt' because it is being used by another process.
```

OSが「もうこれ以上ファイルを開けない！」とエラーを出す。

---

## **まとめ**
| 状況 | ファイルが閉じる？ | リスク |
|------|-----------------|--------|
| `foreach (var line in File.ReadLines("file.txt"))` ですべての行を読む | ✅ 自動的に閉じる | 問題なし |
| `File.ReadLines("file.txt").Take(5)` のように一部の行だけ読む | ❌ ストリームが閉じない | ファイルロック、ハンドルリークの可能性 |
| ループのたびに `File.ReadLines("file.txt")` を呼ぶ | ❌ ストリームが閉じない | 大量のファイルハンドルリーク、エラー発生 |

### **適切な対応**
✅ **すべての行を読むことで、自動でDisposeする**  
✅ **一部の行だけ読むなら `using` で `Dispose()` する**  
✅ **usingを使えない場合は、例外処理を `try-finally` でしっかり行う**

これを意識すれば、ファイルを開きっぱなしにすることなく、安全に `File.ReadLines` を使える。