Chuyển tới nội dung chính

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");
Sử dụng File cho tệp nhỏ (Use File for small files)

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);
}
}
Luôn sử dụng 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);
Luôn sử dụng Path.Combine (Always use Path.Combine)

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, FileInfoDirectoryInfo 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ơnChi 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ạiSiê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 FileInfoPhù 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:

FileModeHành vi (Behavior)
CreateNewTạo tệp mới; ném ngoại lệ nếu đã tồn tại (Creates new file; throws if exists)
CreateTạo tệp mới; ghi đè nếu đã tồn tại (Creates new file; overwrites if exists)
OpenMở 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)
OpenOrCreateMở nếu tồn tại, tạo nếu không (Opens if exists, creates if not)
TruncateMở và làm rỗng tệp đã tồn tại (Opens and empties existing file)
AppendMở 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);
Sử dụng async cho File I/O trong phương thức async (Use async for file I/O in async methods)

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)

  1. Không sử dụng using với luồng — Quên giải phóng StreamReader/StreamWriter/FileStream để handle tệp mở, gây vấn đề khóa và rò rỉ tài nguyên.

  2. 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ụng File.ReadLines (IEnumerable lười - lazy) hoặc StreamReader.ReadLineAsync để xử lý từng dòng.

  3. 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ụng Path.Combine hoặc Path.DirectorySeparatorChar.

  4. 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 IOException cụ thể và xử lý có ý nghĩa thay vì bắt tất cả ngoại lệ.

  5. Điều kiện chạy đua với File.Exists — Kiểm tra File.Exists trướ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ý FileNotFoundException thay thế.

  6. 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 FileStream với FileShare để 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)

  1. Sử dụng lớp tĩnh File/Directory cho thao tác đơn giản, một lần trên tệp nhỏ.
  2. Sử dụng StreamReader/StreamWriter cho tệp lớn hoặc đọc/ghi tăng dần.
  3. Luôn bao luồng trong using để đảm bảo handle tệp được giải phóng.
  4. Sử dụng Path.Combine cho xây dựng đường dẫn đa nền tảng.
  5. Ưu tiên phương thức async (ReadAllTextAsync, ReadLineAsync) trong ứng dụng ASP.NET và UI.
  6. Sử dụng FileInfo/DirectoryInfo khi thực hiện nhiều thao tác trên cùng đường dẫn.
  7. File.ReadLines trả về IEnumerable<string> lười (lazy) — sử dụng thay vì ReadAllLines cho 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 FileFileInfo là gì?

File là 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. FileInfo là 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ụng File cho thao tác đơn; FileInfo cho truy cập lặp lại.

Câu hỏi: Sự khác biệt giữa StreamReaderFileStream là gì?

StreamReader đọc văn bản (ký tự) với xử lý mã hóa tự động — nó bao bọc FileStream nộ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ụng StreamReader cho tệp văn bản; FileStream cho 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ọi Dispose) đó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.ReadLinesFile.ReadAllLines là gì?

ReadAllLines tải toàn bộ tệp vào mảng string[] trong bộ nhớ. ReadLines trả về IEnumerable<string> lười (lazy) đọc từng dòng một lần. Đối với tệp lớn, ReadLines tiế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ụng await để 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.

Tài liệu tham khảo (References)