File I/O
Định nghĩa (Definition)
File I/O (Đầu vào/Đầu ra - Input/Output) trong C# chỉ việc đọc từ và ghi vào tệp và thư mục trên hệ thống tệp. .NET cung cấp nhiều lớp trừu tượng: lớp luồng cấp thấp (low-level stream) (FileStream, StreamReader, StreamWriter), trợ giúp tĩnh cấp cao (high-level static helper) (File, Directory), và lớp bao bọc thông tin (informative wrapper) (FileInfo, DirectoryInfo). Tất cả API File I/O nằm trong không gian tên System.IO.
using System.IO;
// High-level — one-liners for simple operations
string text = File.ReadAllText("data.txt");
File.WriteAllText("output.txt", "Hello, World!");
// Stream-based — fine-grained control for large files
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine("Application started");
Khái niệm cốt lõi (Core Concepts)
Lớp File (File Class)
System.IO.File là lớp tĩnh cung cấp phương thức một lần (one-shot method) cho các thao tác tệp phổ biến. Mỗi phương thức mở và đóng tệp nội bộ — lý tưởng cho tệp nhỏ.
// Reading
string text = File.ReadAllText("data.txt"); // Entire file as string
string[] lines = File.ReadAllLines("data.txt"); // Line-by-line array
byte[] bytes = File.ReadAllBytes("image.png"); // Raw bytes
// Writing
File.WriteAllText("output.txt", "Hello, World!"); // Overwrite with string
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" }); // Overwrite with lines
File.WriteAllBytes("binary.dat", new byte[] { 0x01, 0x02 }); // Write raw bytes
// Appending
File.AppendAllText("log.txt", "New entry\n");
File.AppendAllLines("log.txt", new[] { "Entry 1", "Entry 2" });
// File information
bool exists = File.Exists("data.txt");
DateTime modified = File.GetLastWriteTime("data.txt");
long size = new FileInfo("data.txt").Length;
// Copy, Move, Delete
File.Copy("source.txt", "dest.txt", overwrite: true);
File.Move("old.txt", "new.txt");
File.Delete("temp.txt");
File.ReadAllText / WriteAllText hoàn hảo cho tệp nhỏ. Đối với tệp lớn, sử dụng StreamReader/StreamWriter để tránh tải toàn bộ nội dung vào bộ nhớ cùng lúc.
StreamReader và StreamWriter
Lớp dựa trên luồng (stream-based) để đọc và ghi từng dòng hoặc từng ký tự. Chúng xử lý mã hóa tự động và lý tưởng cho thao tác tệp lớn hoặc liên tục.
StreamWriter
// Write to file (overwrites by default)
using (var writer = new StreamWriter("output.txt"))
{
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Write("No newline at end");
}
// Append to existing file
using (var writer = new StreamWriter("log.txt", append: true))
{
writer.WriteLine($"[{DateTime.UtcNow:O}] Application started");
}
// With specific encoding
using var writer = new StreamWriter("output.txt", append: false, Encoding.UTF8);
writer.WriteLine("Unicode content: café, naïve");
StreamReader
// Read entire file
using (var reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
}
// Read line by line
using (var reader = new StreamReader("data.txt"))
{
string? line;
while ((line = reader.ReadLine()) is not null)
{
Console.WriteLine(line);
}
}
// Read character by character
using (var reader = new StreamReader("data.txt"))
{
int ch;
while ((ch = reader.Read()) != -1)
{
Console.Write((char)ch);
}
}
using với luồng (Always use using with streams)Luồng (stream) bao bọc handle tệp không quản lý (unmanaged file handle). Câu lệnh using (hoặc khai báo using) đảm bảo handle được giải phóng ngay khi hoàn thành, ngay cả khi ngoại lệ xảy ra. Nếu không, handle vẫn mở cho đến khi bộ thu gom rác chạy, có thể gây vấn đề khóa tệp (file locking).
Lớp Directory (Directory Class)
System.IO.Directory là lớp tĩnh cho thao tác thư mục:
// Create
Directory.CreateDirectory("output/logs"); // Creates all intermediate directories
// Check existence
bool exists = Directory.Exists("output");
// List contents
string[] files = Directory.GetFiles("output"); // All files
string[] dirs = Directory.GetDirectories("output"); // All subdirectories
string[] allFiles = Directory.GetFiles("output", "*.txt", SearchOption.AllDirectories); // Recursive
// Enumerate — lazy version (better for large directories)
IEnumerable<string> largeFiles = Directory.EnumerateFiles("output")
.Where(f => new FileInfo(f).Length > 1_000_000);
// Move, Delete
Directory.Move("oldPath", "newPath");
Directory.Delete("temp", recursive: true); // Delete directory and all contents
// Common paths
string cwd = Directory.GetCurrentDirectory();
string[] logicalDrives = Directory.GetLogicalDrives();
Lớp Path (Path Class)
System.IO.Path là lớp tĩnh cho thao tác đường dẫn đa nền tảng. Nó xử lý dấu phân cách đường dẫn (\ trên Windows, / trên Linux/macOS) tự động:
// Combining paths
string fullPath = Path.Combine("folder", "subfolder", "file.txt"); // folder/subfolder/file.txt
// Extracting parts
Path.GetDirectoryName("folder/subfolder/file.txt"); // folder/subfolder
Path.GetFileName("folder/subfolder/file.txt"); // file.txt
Path.GetFileNameWithoutExtension("data.txt"); // data
Path.GetExtension("data.txt"); // .txt
Path.ChangeExtension("data.txt", ".csv"); // data.csv
// Temporary files
string tempFile = Path.GetTempFileName(); // Creates empty temp file, returns path
string tempDir = Path.GetTempPath(); // Temp directory path
// Special paths
string docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
Không bao giờ nối đường dẫn bằng + hoặc nội suy chuỗi. Path.Combine xử lý dấu gạch chéo cuối và dấu phân cách đa nền tảng đúng cách. "folder" + "\\" + "file.txt" sẽ hỏng trên Linux; Path.Combine("folder", "file.txt") thì không.
FileInfo và DirectoryInfo
Khác với lớp tĩnh File/Directory, FileInfo và DirectoryInfo là lớp instance (thể hiện). Tạo chúng khi cần nhiều thao tác trên cùng một tệp hoặc thư mục — chúng lưu đệm thông tin hệ thống tệp để tránh tra cứu lặp lại.
// FileInfo — instance-based file operations
var file = new FileInfo("data.txt");
Console.WriteLine($"Name: {file.Name}"); // data.txt
Console.WriteLine($"Extension: {file.Extension}"); // .txt
Console.WriteLine($"Size: {file.Length} bytes"); // File size
Console.WriteLine($"Created: {file.CreationTime}"); // Creation time
Console.WriteLine($"Modified: {file.LastWriteTime}"); // Last modified
Console.WriteLine($"Directory: {file.DirectoryName}"); // Parent directory
file.CopyTo("backup.txt", overwrite: true);
file.MoveTo("archive/data.txt");
file.Delete();
// DirectoryInfo — instance-based directory operations
var dir = new DirectoryInfo("output");
Console.WriteLine($"Name: {dir.Name}"); // output
Console.WriteLine($"Parent: {dir.Parent}"); // Parent directory
Console.WriteLine($"Root: {dir.Root}"); // Root drive (e.g., C:\)
FileInfo[] allFiles = dir.GetFiles("*", SearchOption.AllDirectories);
DirectoryInfo[] subDirs = dir.GetDirectories();
dir.Create(); // Create if not exists
dir.CreateSubdirectory("logs"); // Create subdirectory
dir.Delete(recursive: true); // Delete with contents
Lớp tĩnh vs lớp instance (Static class vs instance class):
| Tình huống (Scenario) | Sử dụng Tĩnh (Use Static - File/Directory) | Sử dụng Instance (Use Instance - FileInfo/DirectoryInfo) |
|---|---|---|
| Một thao tác (Single operation) | Có — đơn giản hơn | Chi phí tạo instance |
| Nhiều thao tác trên cùng đường dẫn (Multiple operations on same path) | Tra cứu lặp lại | Siêu dữ liệu được lưu đệm, hiệu quả hơn |
| Cần siêu dữ liệu tệp - kích thước, thời gian (Need file metadata - size, times) | Vẫn phải tạo FileInfo | Phù hợp tự nhiên |
| Chuỗi phương thức (Method chaining) | Không hỗ trợ | Được hỗ trợ |
FileStream
Luồng cấp thấp cho truy cập tệp theo byte với kiểm soát tường minh về bộ đệm, di chuyển, và chia sẻ tệp:
// Read and write with FileStream
using var fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write);
byte[] data = Encoding.UTF8.GetBytes("Hello, binary world!");
fs.Write(data, 0, data.Length);
// Read with buffering
using var fs2 = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
var buffer = new byte[1024];
int bytesRead = fs2.Read(buffer, 0, buffer.Length);
string result = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// Seek to specific position
fs2.Seek(0, SeekOrigin.Begin); // Rewind to start
Các tùy chọn FileMode:
| FileMode | Hành vi (Behavior) |
|---|---|
CreateNew | Tạo tệp mới; ném ngoại lệ nếu đã tồn tại (Creates new file; throws if exists) |
Create | Tạo tệp mới; ghi đè nếu đã tồn tại (Creates new file; overwrites if exists) |
Open | Mở tệp đã tồn tại; ném ngoại lệ nếu không tồn tại (Opens existing file; throws if not exists) |
OpenOrCreate | Mở nếu tồn tại, tạo nếu không (Opens if exists, creates if not) |
Truncate | Mở và làm rỗng tệp đã tồn tại (Opens and empties existing file) |
Append | Mở hoặc tạo; di chuyển đến cuối (Opens or creates; seeks to end) |
Thao tác Tệp Bất đồng bộ (Async File Operations)
File I/O có thể chậm, đặc biệt với ổ đĩa mạng hoặc tệp lớn. Phương thức bất đồng bộ (async method) ngăn chặn chặn luồng:
// Async StreamReader
public async Task<string> ReadFileAsync(string path)
{
using var reader = new StreamReader(path);
return await reader.ReadToEndAsync();
}
// Async StreamReader — line by line
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
yield return line;
}
}
// Async StreamWriter
public async Task WriteLogAsync(string path, string message)
{
await using var writer = new StreamWriter(path, append: true);
await writer.WriteLineAsync($"[{DateTime.UtcNow:O}] {message}");
}
// File class async methods (C# 6+)
string text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("output.txt", "Hello, async!");
await File.AppendAllTextAsync("log.txt", "New entry\n");
string[] lines = await File.ReadAllLinesAsync("data.txt");
await File.WriteAllLinesAsync("output.txt", lines);
Trong ứng dụng ASP.NET hoặc UI, luôn sử dụng ReadAllTextAsync / WriteAllTextAsync / StreamReader.ReadLineAsync thay vì phiên bản đồng bộ. File I/O đồng bộ chặn luồng, giảm khả năng mở rộng (scalability).
Các Mẫu Phổ biến (Common Patterns)
Đọc dữ liệu kiểu CSV (Reading CSV-like data)
var records = new List<(string Name, int Age, string City)>();
foreach (string line in File.ReadLines("people.csv").Skip(1)) // Skip header
{
string[] parts = line.Split(',');
records.Add((parts[0], int.Parse(parts[1]), parts[2]));
}
Ghi log vào tệp (Logging to file)
public class FileLogger
{
private readonly string _logPath;
private readonly Lock _lock = new(); // .NET 9+ Lock; use object for older versions
public FileLogger(string logPath) => _logPath = logPath;
public void Log(string message)
{
lock (_lock)
{
File.AppendAllText(_logPath, $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] {message}\n");
}
}
public async Task LogAsync(string message)
{
await File.AppendAllTextAsync(
_logPath,
$"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] {message}\n");
}
}
Xử lý tệp tạm thời (Temporary file handling)
// Create a temp file — automatically named, in temp directory
string tempFile = Path.GetTempFileName();
try
{
File.WriteAllText(tempFile, "Temporary data");
// Process...
}
finally
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
Khi nào Sử dụng (When to Use)
| Tình huống (Scenario) | API Đề xuất (Recommended API) |
|---|---|
| Đọc/ghi tệp nhỏ (Small file read/write) | File.ReadAllText / WriteAllText |
| Xử lý tệp lớn (Large file processing) | StreamReader / StreamWriter |
| Nhiều thao tác trên cùng tệp (Multiple operations on same file) | FileInfo instance |
| Duyệt thư mục (Directory traversal) | Directory.EnumerateFiles (lazy) |
| Thao tác đường dẫn (Path manipulation) | Path.Combine, Path.GetFileName |
| Dữ liệu nhị phân (Binary data) | FileStream hoặc File.ReadAllBytes |
| Ngữ cảnh async (ASP.NET, UI) (Async context) | ReadAllTextAsync, StreamReader.ReadLineAsync |
| Phân tích CSV hoặc theo dòng (CSV or line-based parsing) | File.ReadLines (lazy, tiết kiệm bộ nhớ) |
Lỗi thường gặp (Common Pitfalls)
-
Không sử dụng
usingvới luồng — Quên giải phóngStreamReader/StreamWriter/FileStreamđể handle tệp mở, gây vấn đề khóa và rò rỉ tài nguyên. -
Tải tệp lớn vào bộ nhớ —
File.ReadAllTextđọc toàn bộ tệp thành một chuỗi. Đối với tệp vài MB trở lên, sử dụngFile.ReadLines(IEnumerablelười - lazy) hoặcStreamReader.ReadLineAsyncđể xử lý từng dòng. -
Cứng hóa dấu phân cách đường dẫn — Sử dụng
"folder\\file.txt"sẽ hỏng trên Linux/macOS. Luôn sử dụngPath.CombinehoặcPath.DirectorySeparatorChar. -
Nuốt IOException — Thao tác tệp có thể thất bại vì nhiều lý do (quyền, đĩa đầy, tệp đang sử dụng). Bắt
IOExceptioncụ thể và xử lý có ý nghĩa thay vì bắt tất cả ngoại lệ. -
Điều kiện chạy đua với File.Exists — Kiểm tra
File.Existstrước khi đọc/ghi không đáng tin cậy — trạng thái tệp có thể thay đổi giữa lần kiểm tra và thao tác. Xử lýFileNotFoundExceptionthay thế. -
Không xử lý khóa tệp — Một tiến trình khác có thể đang mở tệp. Sử dụng
FileStreamvớiFileShaređể kiểm soát chia sẻ, hoặc thử lại với thời gian chờ cho tệp bị khóa.
Tóm tắt (Key Takeaways)
- Sử dụng lớp tĩnh
File/Directorycho thao tác đơn giản, một lần trên tệp nhỏ. - Sử dụng
StreamReader/StreamWritercho tệp lớn hoặc đọc/ghi tăng dần. - Luôn bao luồng trong
usingđể đảm bảo handle tệp được giải phóng. - Sử dụng
Path.Combinecho xây dựng đường dẫn đa nền tảng. - Ưu tiên phương thức async (
ReadAllTextAsync,ReadLineAsync) trong ứng dụng ASP.NET và UI. - Sử dụng
FileInfo/DirectoryInfokhi thực hiện nhiều thao tác trên cùng đường dẫn. File.ReadLinestrả vềIEnumerable<string>lười (lazy) — sử dụng thay vìReadAllLinescho tệp lớn.
Câu hỏi Phỏng vấn (Interview Questions)
Câu hỏi: Sự khác biệt giữa File và FileInfo là gì?
Filelà lớp tĩnh mà mỗi phương thức thực hiện kiểm tra bảo mật và tra cứu tệp.FileInfolà lớp instance lưu đệm siêu dữ liệu tệp, hiệu quả hơn cho nhiều thao tác trên cùng tệp. Sử dụngFilecho thao tác đơn;FileInfocho truy cập lặp lại.
Câu hỏi: Sự khác biệt giữa StreamReader và FileStream là gì?
StreamReaderđọc văn bản (ký tự) với xử lý mã hóa tự động — nó bao bọcFileStreamnội bộ.FileStreamđọc byte thô và cho bạn kiểm soát cấp thấp về chế độ truy cập tệp, chia sẻ, và di chuyển. Sử dụngStreamReadercho tệp văn bản;FileStreamcho dữ liệu nhị phân.
Câu hỏi: Tại sao nên sử dụng using với luồng?
Luồng giữ handle tệp không quản lý không được giải phóng kịp thời bởi bộ thu gom rác.
using(gọiDispose) đóng handle ngay lập tức, ngăn chặn rò rỉ tài nguyên và vấn đề khóa tệp.
Câu hỏi: Sự khác biệt giữa File.ReadLines và File.ReadAllLines là gì?
ReadAllLinestải toàn bộ tệp vào mảngstring[]trong bộ nhớ.ReadLinestrả vềIEnumerable<string>lười (lazy) đọc từng dòng một lần. Đối với tệp lớn,ReadLinestiết kiệm bộ nhớ hơn đáng kể.
Câu hỏi: Bạn xử lý File I/O bất đồng bộ trong C# như thế nào?
Sử dụng phương thức async như
File.ReadAllTextAsync,File.WriteAllTextAsync,StreamReader.ReadLineAsync, vàStreamWriter.WriteLineAsync. Chúng sử dụngawaitđể giải phóng luồng trong khi I/O hoàn thành, cải thiện khả năng mở rộng (scalability) trong ASP.NET và khả năng phản hồi trong ứng dụng UI.