1. Giới thiệu
Hãy tưởng tượng một cái ấm nước. Bạn mở vòi nước — nước bắt đầu chảy. Bạn có thể lấy đầy nước một lần, hoặc rót vào ấm từng chút một. Làm việc với file cũng vậy — không phải lúc nào cũng tiện hoặc có thể tải toàn bộ file vào bộ nhớ ngay lập tức. File có thể rất lớn, và đôi khi nguồn dữ liệu không phải là file mà là, ví dụ, kết nối mạng, nơi dữ liệu đến dần dần.
Nếu lúc nào cũng làm việc với mảng byte, thì với file lớn bộ nhớ sẽ nhanh chóng hết, và với các dòng dữ liệu "vô tận" (ví dụ, video hoặc audio stream) thì cách này không ổn chút nào. Đó là lúc khái niệm dòng xuất hiện để cứu nguy!
Trong .NET, dòng là một abstraction cho truy cập tuần tự tới dữ liệu: không quan trọng nguồn là gì — file, mạng, bộ nhớ, hay thậm chí là thứ gì đó lạ như file nén. Dòng cho phép bạn đọc và ghi dữ liệu từng phần, thường là theo block hoặc byte.
Ý tưởng chính:
- Dòng — là kênh truyền dữ liệu. Nó giống như băng chuyền: bạn có thể "đặt" (ghi) hoặc "lấy" (đọc) dữ liệu mà không cần quan tâm chi tiết nó lưu ở đâu và như thế nào.
- Dữ liệu đến tuần tự: bạn chỉ có thể đọc phần tiếp theo sau khi đã đọc phần trước (hoặc ngược lại nếu hỗ trợ tua lại).
- Phần lớn trường hợp bạn không lưu toàn bộ dữ liệu vào bộ nhớ cùng lúc (máy tính sẽ cảm ơn bạn vì điều này).
Abstraction này là nền tảng cho hầu hết các thao tác vào-ra trong .NET: làm việc với file, mạng, file nén, thậm chí cả console!
2. Dòng System.IO.Stream
Kế thừa và kiến trúc: System.IO.Stream
Hầu hết các dòng trong .NET đều kế thừa từ lớp abstract System.IO.Stream. Nó định nghĩa các phương thức cơ bản để đọc, ghi, di chuyển trong dòng và quản lý dòng.
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 — lớp abstract cơ bản
- FileStream — làm việc với file
- MemoryStream — làm việc với dữ liệu trong bộ nhớ
- NetworkStream — giao tiếp mạng
- CryptoStream — mã hóa/giải mã
Làm quen nhanh với các thuộc tính và phương thức chính của dòng
| Thuộc tính / Phương thức | Mô tả |
|---|---|
|
Có thể đọc từ dòng này không |
|
Có thể ghi vào dòng này không |
|
Có thể di chuyển trong dòng không (không phải dòng nào cũng hỗ trợ) |
|
Độ dài dòng (nếu hỗ trợ — không phải dòng nào cũng có) |
|
Vị trí hiện tại trong dòng |
|
Đọc dữ liệu |
|
Ghi dữ liệu |
|
Di chuyển trong dòng |
|
Xả buffer (ghi toàn bộ dữ liệu đã tích lũy vào dòng) |
/ |
Đóng dòng và giải phóng tài nguyên |
Cùng xem nó trông như thế nào "trong thực tế".
3. Ví dụ: đọc và ghi file qua Stream
Đây là ví dụ tối giản để thấy dòng "hoạt động thực tế":
// Mở file để ghi
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Giả sử muốn ghi các số từ 1 đến 10 vào file
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Ghi từng byte một
}
// Đóng file rõ ràng để mở lại cho việc đọc
stream.Close();
// Giờ thử đọc lại các số này
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Sẽ in ra 1, 2, ... 10
}
Ở đây ta dùng FileStream, đúng nghĩa là một dòng thực thụ: bạn đọc và ghi dữ liệu theo block hoặc từng byte.
Các loại dòng: gặp ở đâu?
Dòng không nhất thiết chỉ là file trên ổ cứng. Đây là vài ví dụ nơi dùng khái niệm dòng:
- File trên ổ cứng (ví dụ, FileStream — trường hợp phổ biến nhất)
- Dòng trong bộ nhớ RAM (MemoryStream — tiện cho dữ liệu tạm/thời gian ngắn)
- Kết nối mạng (NetworkStream)
- Nén/giải nén (GZipStream, DeflateStream)
- Mã hóa (CryptoStream)
- Console input/output (đúng vậy!) — về mặt kỹ thuật cũng là dòng
Nhờ vậy bạn có thể viết code mà không cần quan tâm nguồn/đích dữ liệu cụ thể: chỉ cần code làm việc với dòng là đủ "đa năng"!
4. Những lưu ý hữu ích
Đọc và ghi là thao tác truyền dữ liệu từng phần. Thường dùng mảng byte và các phương thức Read, Write.
Ví dụ: đọc file theo block
byte[] buffer = new byte[1024]; // Buffer 1024 byte (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Xử lý chỉ bytesRead byte trong buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Tổng block: {sum}");
}
Cách này dùng ở khắp nơi — từ phần mềm diệt virus đến trình phát nhạc.
Định vị trong dòng (Position, Seek)
Hầu hết các dòng (ví dụ, dòng file) đều có thể di chuyển trong dữ liệu — không chỉ đọc "phần tiếp theo" mà còn nhảy tới vị trí cụ thể để làm việc.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Nhảy tới byte thứ 6 (đánh số từ 0)
int value = stream.ReadByte();
Console.WriteLine($"Byte thứ 6 trong file: {value}");
Dòng chỉ đọc, chỉ ghi, hoặc cả hai
Một số dòng chỉ hỗ trợ một kiểu thao tác:
- File mở để ghi: chỉ Write()
- Dòng đọc dữ liệu mạng: chỉ Read()
- Một số trường hợp đặc biệt (ví dụ, dòng in ra máy in) không thể "quay lại" hay định vị trong dòng (không tua lại được).
Hãy kiểm tra các thao tác hỗ trợ bằng thuộc tính CanRead, CanWrite, CanSeek:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Hỗ trợ đọc");
if (stream.CanWrite)
Console.WriteLine("Hỗ trợ ghi");
if (stream.CanSeek)
Console.WriteLine("Có thể di chuyển trong file");
Buffer trong dòng
Hầu hết các dòng đều dùng buffer nội bộ để tăng hiệu suất. Buffer giúp giảm số lần truy cập ổ cứng/mạng: dữ liệu được tích lũy bên trong rồi mới ghi/đọc một lần.
Phương thức Flush() giúp xả buffer (ví dụ, để đảm bảo mọi thứ đã ghi ra ổ cứng):
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(); // Đảm bảo dữ liệu đã ghi ra ổ cứng
Nếu bạn ghi dữ liệu quan trọng (ví dụ, giao dịch thanh toán!), hãy gọi Flush() thường xuyên.
5. Lỗi thường gặp khi làm việc với dòng
Người mới hay mắc các lỗi sau:
Quên đóng dòng (dẫn đến rò rỉ bộ nhớ, file bị "treo" và nhiều chuyện vui khác).
Nhầm lẫn giữa dòng text và dòng nhị phân — cố ghi chuỗi bằng phương thức byte, sau đó nhận về "ký tự lạ".
Dùng buffer quá nhỏ (hoặc không dùng buffer) — thao tác sẽ rất chậm.
Nghĩ rằng Read() luôn đọc đúng số byte yêu cầu — thực tế có thể trả về ít hơn; luôn phải kiểm tra giá trị trả về.
Không để ý rằng không phải dòng nào cũng hỗ trợ di chuyển (Seek), nhất là dòng mạng.
Ví dụ:
// Ví dụ tệ: đọc tất cả byte file mà không kiểm tra số byte thực sự đọc được
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead có thể nhỏ hơn 1024 nếu file nhỏ hơn!
}
GO TO FULL VERSION