1. 介紹
處理小檔案時常常什麼都感覺不到:寫了、讀了 — 然後忘了。但一旦檔案大小開始超過至少 100–500 MB,更別說到 GB,會出現一些有趣的現象:
- 操作變慢,特別是直接「硬上」處理時 — 比如 File.ReadAllBytes() 或 File.WriteAllText() 可能會突然拖垮整個程式。
- 可能會耗盡 RAM,出現 OutOfMemoryException。
- 系統開始使用 swap(分頁檔),這會導致整個系統變慢。
- 並行操作可能會對磁碟造成過度負載。
在真實任務中這類情況很常見:
- 伺服器日誌(一天幾 GB)。
- 處理大型 CSV 或 XML 檔案的匯入/匯出。
- 處理影片、音訊、壓縮檔、二進位檔案。
- 複製大型組件或做備份。
2. 處理大型檔案的優化策略
在開始優化之前,先弄清楚你想加速或改善什麼。常見目標有:
- 「儘可能快地讀/寫檔案,同時不把系統拉垮」。
- 「分段處理檔案,避免佔滿記憶體」。
- 「不要在記憶體裡生成多餘的資料副本」。
- 「如果可能的話,並行處理大型檔案」。
通用做法:
- 串流式讀寫:使用 stream 和 buffer,分段讀寫(FileStream、BufferedStream)。
- 直接在磁碟上操作檔案 — 避免在記憶體中做中介複製。
- 謹慎管理緩衝與記憶體:不要把整個檔案保存在 RAM 裡。
- 非阻塞的 async 操作 — 如果你不想阻塞主執行緒(下一堂會詳談)。
3. 串流式讀寫:基本範式
核心原則:用分段的方式處理檔案! 在現代 C# 裡,使用 FileStream、BufferedStream 等類別很容易做到。
// 打開一個讀取用的 stream:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;
// 一直讀到檔案結尾
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// 在這裡處理讀到的資料!
// 比如練習用:計算所有位元組的總和
long sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"已讀取 {bytesRead} 位元組,總和: {sum}");
}
建議:緩衝大小(例如 64 KB、128 KB、1 MB)應該透過實驗來調整。太小會導致大量磁碟存取;太大不一定有更好效能,卻會消耗更多記憶體。
為什麼這樣做?用 File.ReadAllBytes() 或 File.ReadAllText() 這種不分段的讀法會試著把整個內容載入記憶體。檔案一大 — 結果很明顯:你會碰到 OutOfMemoryException。
4. BufferedStream:為何與何時使用
我們之前的講座提過這個類別,這裡再提醒:BufferedStream 是任何 stream 的包裝器,可以讓讀/寫不是單位位元組而是塊狀操作。
使用範例:
using var fileStream = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
using var bufferedStream = new BufferedStream(fileStream, 1024 * 128);
byte[] buffer = new byte[1024 * 128];
int bytesRead;
while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0)
{
// 處理資料
}
有時候用 BufferedStream 會提速,特別是你一次只讀幾個位元組,而檔案系統偏好區塊存取時。
有趣的事實:在新版的 FileStream 中已經內建了緩衝機制,所以手動包一層 BufferedStream 最有益的情境是操作「原始」的 stream,例如網路流或非標準裝置。
5. 讀寫大型文字檔案
二進位檔案比較直觀:分段讀寫。但文字檔,像大型 CSV、日誌、JSON 要怎麼做?
這時候可以用 StreamReader 來讀,StreamWriter 來寫。
逐行讀取:
using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
// 處理這一行
if (line.Contains("ERROR"))
Console.WriteLine("發現 ERROR: " + line);
}
為什麼這樣好?
- 我們不會把整個檔案保存在記憶體中。
- 而且 StreamReader 內部的緩衝通常已經被調校得不錯。
逐行寫入:
using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
writer.WriteLine($"這是第 {i} 行");
串流式讀取的架構
[磁碟上的檔案]
|
[FileStream]
|
[BufferedStream (可選)]
|
[StreamReader/StreamWriter (處理文字)]
|
[你的程式碼:處理資料]
6. 實作範例
我們繼續發展處理日誌的應用。假設要把 7 天以前的舊日誌壓縮成一個檔案。若某個日誌非常大,我們就分段讀寫,避免把記憶體「埋爆」。
簡化處理,可以用標準的 stream 複製範例:
void CopyLargeFile(string sourcePath, string destPath)
{
using var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
using var destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
byte[] buffer = new byte[1024 * 256]; // 256 KB
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
destStream.Write(buffer, 0, bytesRead);
// 可以在這裡加進度條!
}
}
應用場景:
- 備份檔案的複製
- 按天或月合併日誌
- 檔案的預處理(例如行過濾)
7. 如何評估緩衝的效能?
有時你會想知道:「到底快了多少?」可以用計時來比較:
var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"複製時間: {watch.Elapsed.TotalSeconds} 秒");
常見的緩衝值:
- 4 KB — 最小檔案系統區塊。
- 64 KB / 128 KB — 實務上幾乎都表現良好。
- 1 MB 或以上 — 只有在非常快速的 SSD 與超大型檔案時才有意義。
你可以自己試不同的緩衝大小!
8. 在大型檔案中搜尋與處理資料
如果不是單純複製,而是要搜尋特定行、數字或片語,分段處理仍然最有效。
範例:在龐大日誌中找出所有出現錯誤的行
using var reader = new StreamReader("server.log");
using var writer = new StreamWriter("errors.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
if (line.Contains("ERROR"))
writer.WriteLine(line); // 只把需要的行寫入結果
}
這種方式可以處理數 GB 甚至更多的日誌,而不會耗盡大量記憶體。
9. 處理超大型檔案(>2 GB)的注意事項
只要用串流式讀寫,.NET 與 Windows 對任何大小的檔案(甚至數十 TB)都能正常工作。但有些細節要注意!
- 32 位元應用程式有 2 GB 位址空間限制 — 請使用 x64!
- 對於大檔案,請使用 64 位元平台(AnyCPU 或 x64)。
- 若檔案超過 4 GB,檔案系統 FAT32 不適合 — 要用 NTFS 或 exFAT。
對大型檔案做迭代處理
+------------------+
| Start |
+------------------+
|
v
+------------------------------+
| 打開讀取的 stream |
+------------------------------+
|
v
+------------------------------+
| 當尚未達到檔案結尾 |
+------------------------------+
|
v
+---------------------------+
| 讀取一個資料區塊 |
+---------------------------+
|
v
+---------------------------+
| 處理該區塊 |
+---------------------------+
|
v
+--------------------------+
| 下一個區塊 |
+--------------------------+
|
v
+--------------------+
| 關閉 stream |
+--------------------+
10. 有用的細節
把檔案當成 stream:FileStream 的常用方法
| 方法 | 說明 |
|---|---|
|
把部分檔案讀到緩衝區 |
|
把緩衝區的部分資料寫到檔案 |
|
跳到檔案中的指定位置 |
|
檔案大小(位元組) |
|
檔案中的當前位置 |
使用 Seek() 的範例:
using var stream = new FileStream("bigfile.bin", FileMode.Open);
// 往前跳 1 GB!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// 現在從檔案中間讀資料!
何時會用到?
- 為檔案做索引
- 快速存取特定區塊(例如大型資料庫中的區塊)
多執行緒處理檔案
如果任務允許,可以並行處理大型檔案:把檔案分成多個區塊,然後同時讀/寫不同的部分。但要明白 HDD 在隨機存取下會變慢,SSD 則快得多。
在一般家用情況下,如果不確定就順序處理。並行更適合同時處理多個檔案,而不是把單一檔案拆成多執行緒(否則效益可能有限)。
11. 處理大型檔案時常見錯誤
錯誤 #1:把整個檔案讀到記憶體。
新手常用 File.ReadAllBytes() 或 File.ReadAllText() 去讀超大檔案。若檔案是 GB 級別,程式會直接崩潰 — 記憶體不夠。應該用串流式讀取。
錯誤 #2:使用過小的緩衝。
用超小的緩衝讀檔就像用茶匙喝湯。程式會花很多時間在磁碟存取上,效率很差。請選合理的緩衝大小。
錯誤 #3:忘記關閉 stream。
如果工作結束後不關閉 stream,檔案描述符會被佔用,會影響其他程式並可能導致 OS 層級的錯誤。一定要使用 using — 這樣比較安全也比較乾淨。
錯誤 #4:同時從多處存取同一個檔案。
嘗試同時從不同程式或不同區塊讀寫同一檔案通常會導致 IOException。即便有時「看起來能跑」,也很難保證穩定性。
GO TO FULL VERSION