CodeGym /課程 /C# SELF /類別 FileStream

類別 FileStream

C# SELF
等級 35 , 課堂 3
開放

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 方法(ReadAsyncWriteAsync),讓你的程式不卡卡。
  • 部分讀寫或隨機存取:如果你要從檔案某個位置(像 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:怎麼開檔案(OpenCreateAppendOpenOrCreate 等)
  • FileAccess:要對檔案做什麼(ReadWriteReadWrite
  • FileShare:其他 process 能不能同時用這個檔案(通常簡單任務用不到)

常見錯誤:如果你忘了關掉 FileStream,檔案可能會「被鎖住」——之後就不能刪或重開了!

3. FileStream 建構子的參數

FileStream 的建構子可以很細地設定你要怎麼開檔案。主要參數如下:

new FileStream(
    string path,           // 檔案路徑(絕對或相對)
    FileMode mode,         // 怎麼開檔(建立、開啟、覆寫等)
    FileAccess access,     // 權限(只讀、只寫、讀寫)
    FileShare share        // 其他 process 開這檔案時的權限(可選)
);

來看看 FileModeFileAccess 的列舉值:

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. 處理大檔案

FileStreamFile.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 權限不足、FileNotFoundExceptionOpen 模式開不存在的檔案)很常見。一定要把 FileStream 操作包在 try-catch 裡。

編碼:FileStream 處理文字資料(它只管 bytes),你要自己選對編碼Encoding.UTF8Encoding.ASCIIEncoding.Unicode 等)來轉字串和 bytes。選錯編碼會出現亂碼。

控制位置:如果你用 Seek(),要小心 offsetorigin,不要跳到檔案外面或奇怪的位置。

檔案鎖定(FileShare):如果你用 FileShare.None 開檔,其他 process 不能碰這檔案。如果別的程式(或你自己程式的另一個 thread)要用同一個檔案就會出問題。一定要選最適合的 FileShare 模式。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION