1. 介紹
在開始使用新的類別之前,先搞清楚它到底為什麼存在比較好。讓我們弄清楚當我們用 FileStream「直接」操作檔案時到底發生了什麼。
當你呼叫 Read 或 Write 對一個由 FileStream 建立的 stream 時,實際上是在跟磁碟子系統互動。這個過程本身(尤其是在舊式的 HDD 上,但在新的 SSD 上也一樣)比操作 RAM 慢很多。想像一下你在 McDonalds 點薯條,但每次收銀員都要去倉庫拿一袋新的包裝,看看隊伍會有多長!
如果你每次只處理小塊資料,頻繁地訪問磁碟或網路會導致性能大幅下降。資料量越大,這個差異越明顯。
簡短類比
沒緩衝的 stream 大概就像跑去超市十次,每次只買一個優格;緩衝式 stream 則是一次帶一整籃優格,把跑腿次數降到最低。
2. 類別 BufferedStream:第一印象
它的用途
BufferedStream 是包在任何 stream(Stream)外面的封裝器,它在記憶體中維護一個中間緩衝區。當你寫入資料時,資料會先進入緩衝區,只有當緩衝區被填滿時才會一次性寫到磁碟。讀取也是類似:第一次讀會把一大塊資料載入記憶體,之後從記憶體中逐塊返回,直到緩衝耗盡。
程式範例:建立 BufferedStream
來看個最簡範例。假設我們要寫入 100,000 行到檔案:
string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);
for (int i = 0; i < 100_000; i++)
{
writer.WriteLine($"第{i}行");
}
Console.WriteLine("寫入完成!");
說明:
- 我們透過 FileStream 開啟檔案以供寫入。
- 然後把它包在 BufferedStream,再用 StreamWriter(它負責把文字寫進 stream)。
- 只要緩衝區被填滿,資料就會一次性寫到磁碟。
3. 緩衝的內部運作
我們用個流程圖來拆解:
[你的程式碼] → [StreamWriter] → [BufferedStream] → [FileStream] → [磁碟上的檔案]
當你對 StreamWriter 呼叫 WriteLine() 時,文字會先寫進它自己的內部緩衝,接著透過 BufferedStream 進入另一個緩衝,最後當緩衝滿了或 stream 被關閉時,資料才寫到磁碟。
一個桶能裝多少位元組?
預設緩衝大小是 4096 byte(4 KB),但你可以明確指定:
int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...
實用小技巧: 在現代系統上,合理的緩衝大小大約是 8–64 KB。對於非常大的檔案操作可以更大。但別過頭:如果你在記憶體只有 128 KB 的微控制器上工作,分配 64 KB 的緩衝就不是好主意 :)
4. 實驗:比較有無緩衝的速度
為了看清差別,我們寫個測試,用有緩衝與無緩衝的方式來比較寫入速度:
using System.Diagnostics;
using System.Text;
string data = new string('X', 1000); // 1 000 字元
void WriteWithoutBuffer()
{
using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
fs.Write(bytes, 0, bytes.Length); // 直接寫入檔案 — 每次都會存取磁碟
}
}
void WriteWithBuffer()
{
using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
using var bs = new BufferedStream(fs, 16 * 1024);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
bs.Write(bytes, 0, bytes.Length);
}
}
// 計時
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("未使用緩衝: " + sw.ElapsedMilliseconds + " 毫秒");
sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("使用緩衝: " + sw.ElapsedMilliseconds + " 毫秒");
預期結果:
大多數情況下有緩衝會顯著快很多!尤其是在 HDD 上。如果是 SSD,效果仍然存在,但不會那麼戲劇化。
5. 選用哪種緩衝?比較與實務
.NET 裡有很多用來緩衝的類別,這裡幫你整理清楚:
| 類別 | 用途 | 內建緩衝? | 需要使用 BufferedStream? |
|---|---|---|---|
|
處理檔案 | 是(4 KB) | 幾乎不需要(但可以用) |
|
處理網路 | 否 | 強烈建議 |
|
讀取/寫入文字 | 是(從 1 KB 起) | 通常不需要 |
|
壓縮/解壓縮 | 否 | 可/建議用來加速 |
重要:
如果你在建立 FileStream 時已經在建構子裡指定了合適的 bufferSize,那它本身就已經是有緩衝的了。這種情況下再加一層 BufferedStream 不會帶來太大提升。但對於沒有緩衝的 stream(例如網路流)或自訂的 Stream 實作,BufferedStream 就很有用了。
6. 範例:用 BufferedStream 複製檔案
string source = "big_input.dat";
string dest = "big_output.dat";
int bufferSize = 64 * 1024; // 64 KB
using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
bufferedOutput.Write(buffer, 0, bytesRead);
}
// 別忘了 flush — 否則最後一些位元組可能不會寫到磁碟!
bufferedOutput.Flush();
Console.WriteLine("複製完成!");
說明:
- 從一個檔案透過 BufferedStream 以較大的區塊(64 KB)讀取資料。
- 再把資料寫到另一個檔案,同樣使用緩衝。
- 迴圈結束後務必呼叫 Flush(),把最後的資料寫出去。
7. 實用細節
建議:什麼時候確實需要 BufferedStream
- 當你在使用沒有內建緩衝的 stream(像是網路 stream,或自訂繼承自 Stream 的實作);
- 當你在處理大量的二進位資料(例如複製檔案、格式轉換、備份);
- 當你在優化既有程式,發現瓶頸是大量小的 Write/Read 操作。
關於非同步和緩衝的一點說明
有了非同步操作(ReadAsync/WriteAsync),緩衝依然有用,但記得:當你在緩衝上使用非同步方法時,處理仍然會在記憶體層面做緩衝,實際的磁碟 I/O 會被進一步減少。
在 .NET 8+ 和 .NET 9 裡,緩衝機制越來越深入內建,很多類別預設就有緩衝。但為了跟網路 stream 或你自己的實作相容,手動使用 BufferedStream 仍然很有幫助。
關於非同步的更多內容你會在第 58 級學到 :P
緩衝式 stream 的視覺化示意
flowchart LR
A[你的程式碼] --> B[StreamReader/Writer]
B --> C[BufferedStream]
C --> D[FileStream]
D --> E[檔案/裝置]
- A — 你的程式碼,會呼叫 Write/Read。
- B — 高階的 stream(處理文字或資料)。
- C — 緩衝(把資料分組以提升速度)。
- D — 具體的 stream 實作(檔案、網路)。
- E — 實體裝置(硬碟、SSD、網路等)。
實戰技巧和小貼士
- 如果你每次只寫一行到檔案(像是 logging),最好把緩衝大小設定大於單行大小。這樣可以更快地一次性寫出大批資料。
- 如果每個操作都必須立即寫入(例如關鍵日誌),寫完就呼叫 Flush()。但要注意這會降低緩衝帶來的效益!
- 如果你建立的是暫存檔,而且建立後馬上就刪除,緩衝裡還有沒刷新的資料可能不那麼重要 — 不過如果需要確保檔案完整,還是要小心。
- 處理非常大的檔案(比如幾十 GB)時,可以把緩衝提高到 1_048_576 byte(1 MB)或更大 — 只要記憶體允許。
8. 常見錯誤和使用細節
如果你現在很想「到處塞緩衝」——先冷靜一下。凡事適度最重要!
常見錯誤之一是忘了呼叫 Flush() 或在該關閉 stream 時沒有正確關閉。如果程式在 stream 關閉前崩潰,最後的幾個位元組可能還留在記憶體中的緩衝,沒被寫到磁碟。例如寫日誌時程式當掉,最後一條記錄可能就不見了。
BufferedStream 並不理解你的邏輯訊息邊界 — 它只是等到累積到某個資料量才一次性發送。因此對於關鍵性資料(像日誌、備份等),最好定期強制呼叫 Flush():
bufferedStream.Flush(); // 強制把緩衝資料寫到磁碟
如果你在使用 StreamWriter,要記得它也有自己的緩衝!也就是說巢狀使用時會有兩層緩衝(這不一定是好事)。通常只要一層緩衝就夠了,如果你已經使用 StreamWriter,額外再套 BufferedStream 未必必要。
GO TO FULL VERSION