1. 類別 FileStream:分段操作
你可以把 FileStream 想像成一條直接連到你硬碟檔案的水管。透過這條水管你可以很精細地控制資料流動:把 bytes 寫進檔案(寫入)或從檔案拿出 bytes(讀取)。跟那些高階方法只會「給你一杯水」不一樣,FileStream 是直接讓你操作「水龍頭」,想怎麼開關水(bytes)都可以自己來。
FileStream 是在 byte 層級運作的。也就是說,它根本不管你是文字還是圖片,對它來說一切都是 bytes 的序列。如果你用 FileStream 處理文字檔,你就要自己把字串轉成 bytes(用 編碼,像是 UTF-8)來寫入,讀出來時也要自己轉回字串。
什麼時候一定要用 FileStream?
- 處理超大檔案:當檔案大到沒辦法或不適合整個載入記憶體(像是幾 GB 的 log、影片檔)。FileStream 讓你可以分段(chunk)讀寫,記憶體用得更有效率。
- 二進位資料:如果你在處理不是純文字的檔案(圖片、音訊、影片、序列化物件、資料庫檔案),FileStream 就是你的主力,因為它直接給你 bytes。
- 細緻控制存取模式:你要同時開檔案來讀 *和*寫?還是要讓其他程式不能碰這個檔?或反過來讓多個 process 可以同時讀?FileStream 都能讓你自己決定。
- 非同步操作:在現代高效能應用(像 web server)裡,檔案操作不能卡住主執行緒很重要。FileStream 支援 async 方法(ReadAsync、WriteAsync),讓你的程式不卡卡。
- 部分讀寫或隨機存取:如果你要從檔案某個位置(像 500 byte)開始讀,或要寫進檔案中間,FileStream 可以讓你自己控制檔案指標的位置。
新手常見錯誤:拿 FileStream 來做超簡單的事,比如只寫一行字到小檔案。這種情況用它太 over 了。FileStream 是給特殊、比較複雜場景用的工具。
2. 建立和開啟 FileStream
要開始用 FileStream 操作檔案,你要先 new 一個實例。建構子會吃幾個重要參數,決定你要怎麼跟檔案互動。
using System;
using System.IO; // 一定要加,才能用 FileStream
using System.Text; // 處理編碼(字串和 bytes 互轉)
用 FileStream 讀文字檔的基本範例:
先建立一個檔案,才有東西可以讀。
// 建一個簡單的文字檔來示範
string path = "example_filestream.txt";
// 開啟讀取用的 stream。
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
// 建一個 buffer(byte 陣列)暫存讀到的資料。
byte[] buffer = new byte[fs.Length];
// 從 stream 讀 bytes 到 buffer。
int bytesRead = fs.Read(buffer, 0, buffer.Length);
// 用指定編碼(例如 UTF-8)把 bytes 轉回字串。
string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("用 FileStream 從檔案讀到的內容: " + content);
fs.Close(); //關閉 stream
主要參數
- FileMode:怎麼開檔案(
Open、Create、Append、OpenOrCreate等) - FileAccess:要對檔案做什麼(
Read、Write、ReadWrite) - FileShare:其他 process 能不能同時用這個檔案(通常簡單任務用不到)
常見錯誤:如果你忘了關掉 FileStream,檔案可能會「被鎖住」——之後就不能刪或重開了!
3. FileStream 建構子的參數
FileStream 的建構子可以很細地設定你要怎麼開檔案。主要參數如下:
new FileStream(
string path, // 檔案路徑(絕對或相對)
FileMode mode, // 怎麼開檔(建立、開啟、覆寫等)
FileAccess access, // 權限(只讀、只寫、讀寫)
FileShare share // 其他 process 開這檔案時的權限(可選)
);
來看看 FileMode 和 FileAccess 的列舉值:
FileMode(開檔模式):
決定作業系統開檔時要怎麼處理。
FileMode.Open:開啟已存在的檔案。如果找不到檔案,會丟 FileNotFoundException。FileMode.Create:建立新檔案。如果同名檔案已存在,會被整個覆寫(內容會被清空)。
FileAccess(檔案存取權限):
決定你的程式對開啟的檔案可以做哪些操作。
FileAccess.Read:檔案只開來讀。不能寫入。FileAccess.Write:檔案只開來寫。不能讀取。FileAccess.ReadWrite:檔案同時可讀可寫。這是最彈性的模式,但你要自己管理 stream 的位置。
FileShare(共用存取):
決定其他 process 在你程式開著這檔案時能不能開它。這對避免鎖檔很重要。
FileShare.Read:其他 process 可以讀這檔案,但不能寫。FileShare.Write:其他 process 可以寫這檔案,但不能讀。FileShare.ReadWrite:其他 process 可以讀也可以寫。這是最開放的模式,但如果多個程式同時改檔案可能會衝突。
更多模式細節下堂課再說。
4. 用 FileStream 讀寫資料
用 FileStream 時,你就是在操作 bytes。這代表如果你要處理文字資料,就要自己用 編碼 類別(像 System.Text.Encoding.UTF8)把字串和 byte 陣列互轉。
用 FileStream 寫資料到檔案
寫入時你要先把資料(例如字串)轉成 byte 陣列,再寫進 stream。
string outputPath = "user_data.txt";
string userName = "伊凡 彼得羅夫";
// 用 UTF-8 編碼把字串轉成 bytes
byte[] userNameBytes = Encoding.UTF8.GetBytes(userName);
// 開 FileStream 來寫入。
FileStream fsWrite = new FileStream(outputPath, FileMode.Create, FileAccess.Write)ж
// 把 bytes 陣列寫進 stream。
fsWrite.Write(userNameBytes, 0, userNameBytes.Length);
Console.WriteLine($"名字 '{userName}' 已成功寫入檔案 '{outputPath}'。");
fsWrite.Close(); //關閉 stream
用 FileStream 讀資料
讀取時你要先把 bytes 讀進 buffer,再轉成你要的格式(例如字串)。
string inputPath = "user_data.txt";
// 開 FileStream 來讀取
FileStream fsRead = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
// 建一個跟檔案一樣大的 buffer 來讀全部 bytes。
byte[] buffer = new byte[fsRead.Length];
// 從 stream 讀 bytes 到 buffer。
int bytesRead = fsRead.Read(buffer, 0, buffer.Length);
// 把讀到的 bytes 轉成字串。
string loadedUserName = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"從檔案讀到的名字(用 FileStream): {loadedUserName}");
fsRead.Close(); //關閉 stream
5. 處理大檔案
FileStream 比 File.ReadAll... 方法強的地方,就是能分段讀寫檔案,這對記憶體裝不下的大檔案超重要。
分段讀大檔案
你讀大檔時會用固定大小的 buffer(byte 陣列),一直讀到檔案結尾為止。
string bigFilePath = "bigfile.bin"; //大檔案
int bufferSize = 4096; // buffer 大小,例如 4KB
byte[] buffer = new byte[bufferSize]; // 建 buffer
int bytesRead; // 實際讀到的 bytes 數
long totalBytesRead = 0; // 累計讀到的 bytes
// 開 stream
FileStream fs = new FileStream(bigFilePath, FileMode.Open, FileAccess.Read);
int chunkNumber = 1;
// 只要 fs.Read() 回傳正數就繼續
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
Console.WriteLine($"第 {chunkNumber++} 段: 讀了 {bytesRead} bytes。總共: {totalBytesRead} bytes。");
// !!! 這裡可以處理每一段讀到的資料 !!!
}
fs.Close(); //關閉 stream
重要提醒:方法 fs.Read(buffer, offset, count) 不保證會把 buffer 填滿。它會回傳實際讀到的 bytes 數,可能比 count 少(特別是檔案尾端)。一定要用 bytesRead 來正確處理資料。
分段寫大資料
跟讀一樣,你也可以分段把大量資料寫進檔案。
string bigOutputPath = "big_output.txt";
string longText = new string('A', 1_000_000); // 一百萬個 'A'
byte[] longTextBytes = Encoding.UTF8.GetBytes(longText);
int writeBufferSize = 1024; // 每次寫 1KB
// FileMode.Create: 建新檔案
FileStream fsWrite = new FileStream(bigOutputPath, FileMode.Create, FileAccess.Write);
for (int i = 0; i < longTextBytes.Length; i += writeBufferSize)
{
// 算這次要寫多少 bytes
int bytesToWrite = Math.Min(writeBufferSize, longTextBytes.Length - i);
// 從 longTextBytes 的 'i' 開始寫 bytesToWrite 個 bytes
fsWrite.Write(longTextBytes, i, bytesToWrite);
Console.WriteLine($"寫了 {bytesToWrite} bytes");
}
fsWrite.Close(); //關閉 stream
6. 不明顯的陷阱和常見錯誤
就算 FileStream 這麼強大,還是有一些細節要注意:
緩衝和 Flush():前面提過,資料可能還在記憶體 buffer 裡,還沒真的寫進硬碟。如果你沒用 using 或沒呼叫 Close()/Dispose(),直接結束程式,資料可能會遺失。一定要呼叫 Flush()(或靠 using/Close())來保證資料寫進去。
例外處理:IO 錯誤(像 IOException 寫到滿的硬碟、UnauthorizedAccessException 權限不足、FileNotFoundException 用 Open 模式開不存在的檔案)很常見。一定要把 FileStream 操作包在 try-catch 裡。
編碼:用 FileStream 處理文字資料(它只管 bytes),你要自己選對編碼(Encoding.UTF8、Encoding.ASCII、Encoding.Unicode 等)來轉字串和 bytes。選錯編碼會出現亂碼。
控制位置:如果你用 Seek(),要小心 offset 和 origin,不要跳到檔案外面或奇怪的位置。
檔案鎖定(FileShare):如果你用 FileShare.None 開檔,其他 process 不能碰這檔案。如果別的程式(或你自己程式的另一個 thread)要用同一個檔案就會出問題。一定要選最適合的 FileShare 模式。
GO TO FULL VERSION