1. 前言
想像一下你有一個裝水的熱水壺。你打開水龍頭,水就開始流出來。你可以一次裝滿一大壺水,也可以慢慢加一點一點。檔案也是一樣——不是每次都能或需要把整個檔案一次載入記憶體。檔案可能很大,有時資料來源甚至不是檔案,而是像網路連線這種資料會慢慢來的東西。
如果我們每次都只用 byte 陣列來處理,遇到大檔案很快就會爆記憶體,而且對於「無限」資料流(像影片或音訊串流)這種情境根本行不通。這時候,流(Stream)的概念就超級有用了!
在 .NET 裡,流就是一種讓你可以「順序存取資料」的抽象:不管背後是檔案、網路、記憶體,還是什麼奇怪的東西(像壓縮檔),你都可以分段(通常是區塊或 byte)來讀寫資料。
核心概念:
- 流 就是資料傳輸的通道。很像輸送帶:你可以「放進去」(寫入)或「拿出來」(讀取)資料,不用管資料實際存在哪、怎麼存。
- 資料是順序來的:你只能在讀完前一段後才能讀下一段(或反過來,如果支援倒帶)。
- 大部分情況下,你不會一次把所有資料都塞進記憶體(電腦會感謝你的)。
這個抽象是 .NET 幾乎所有輸入輸出操作的基礎:檔案、網路、壓縮檔,甚至主控台!
2. 流 System.IO.Stream
繼承與架構:System.IO.Stream
幾乎所有 .NET 的流都繼承自抽象類別 System.IO.Stream。它定義了讀取、寫入、移動和管理流的基本方法。
classDiagram
class Stream {
+Read()
+Write()
+Seek()
+CanRead
+CanWrite
+CanSeek
+Length
+Position
}
class FileStream
class MemoryStream
class NetworkStream
class CryptoStream
Stream <|-- FileStream
Stream <|-- MemoryStream
Stream <|-- NetworkStream
Stream <|-- CryptoStream
- Stream —— 基本抽象類別
- FileStream —— 處理檔案用
- MemoryStream —— 處理記憶體資料用
- NetworkStream —— 處理網路資料用
- CryptoStream —— 加解密用
快速認識流的重點屬性和方法
| 屬性 / 方法 | 說明 |
|---|---|
|
這個流能不能讀 |
|
這個流能不能寫 |
|
能不能在流裡移動(不是每種流都支援) |
|
流的長度(如果支援的話,不是每種流都有) |
|
目前在流裡的位置 |
|
讀資料 |
|
寫資料 |
|
在流裡移動 |
|
清空緩衝區(把累積的資料一次寫進流) |
/ |
關閉流並釋放資源 |
來看看實際上怎麼用吧。
3. 範例:用 Stream 讀寫檔案
這裡有個超簡單的範例,讓你看看流「實戰」怎麼玩:
// 開檔案準備寫入
using var stream = new FileStream("numbers.bin", FileMode.Create);
// 假設我們要把 1 到 10 的數字寫進檔案
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // 一次寫一個 byte
}
// 明確關閉檔案,準備再打開來讀
stream.Close();
// 現在來把這些數字讀回來
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // 會印出 1, 2, ... 10
}
這裡我們用 FileStream,它就是最標準的流:你可以分區塊或一個 byte 一個 byte 來讀寫資料。
流的類型:會在哪裡遇到?
流不一定只是磁碟上的檔案。這裡有幾個常見的流應用場景:
- 磁碟檔案(像 FileStream —— 最常見)
- 記憶體流(MemoryStream —— 很適合暫存或中間資料)
- 網路連線(NetworkStream)
- 壓縮/封存(GZipStream, DeflateStream)
- 加密(CryptoStream)
- 主控台輸入/輸出(沒錯!技術上也是流)
這讓你寫的程式不用管資料來源/目的地是什麼:只要是流就能通用!
4. 實用小細節
讀寫就是分段傳輸資料。通常會用 byte 陣列配合 Read、Write 方法。
範例:分區塊讀檔案
byte[] buffer = new byte[1024]; // 1024 byte(1KB)緩衝區
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// 處理 buffer 裡的 bytesRead 個 byte
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"區塊總和: {sum}");
}
這種寫法到處都用得到——從防毒軟體到音樂播放器。
流的定位(Position, Seek)
大多數流(像檔案流)都可以在資料裡移動——你不一定只能讀「下一段」,也可以跳到指定位置開始處理。
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // 跳到第 6 個 byte(從 0 開始算)
int value = stream.ReadByte();
Console.WriteLine($"檔案第 6 個 byte: {value}");
流有只讀、只寫,或兩者皆可
有些流只支援其中一種操作:
- 只寫檔案:只能 Write()
- 網路資料流:只能 Read()
- 有些特殊情況(像印表機輸出流)根本不能倒帶或定位。
用 CanRead、CanWrite、CanSeek 屬性來檢查支援哪些操作:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("支援讀取");
if (stream.CanWrite)
Console.WriteLine("支援寫入");
if (stream.CanSeek)
Console.WriteLine("可以在檔案裡移動");
流的緩衝
幾乎所有流都會用內部緩衝區來加速。緩衝可以減少磁碟/網路的存取次數:資料會先累積,然後一次處理。
Flush() 方法可以強制清空緩衝區(像是保證資料真的寫進磁碟):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // 保證資料真的寫進磁碟
如果你在寫很重要的資料(像金流交易!),記得呼叫 Flush()。
5. 新手常見錯誤
新手最常犯這些錯:
忘記關閉流(結果記憶體外洩、檔案鎖死,各種災難)。
搞混文字流和二進位流——用 byte 方法寫字串,結果讀出來全是亂碼。
緩衝區太小(或根本沒緩衝)——操作超慢。
以為 Read() 一定會讀到你要的 byte 數——其實可能更少;一定要檢查回傳值。
沒注意不是所有流都能定位(Seek),尤其是網路流。
像這樣:
// 壞範例:沒檢查實際讀到多少 byte 就全用
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// 如果檔案小於 1024,bytesRead 會更小!
}
GO TO FULL VERSION