1. Giới thiệu
Trong lập trình, an toàn khi làm việc với tệp không chỉ là bảo vệ khỏi virus và hacker, mà còn là cách xử lý lỗi, khóa, quyền truy cập, đường dẫn sai và các cái bẫy khác. Làm lơ có thể dẫn đến mất dữ liệu, chương trình treo, bug kỳ lạ hoặc ngoại lệ đáng sợ IOException.
Đây là các tình huống điển hình mà chúng ta cần biết cách xử lý:
- Tệp không tồn tại nhưng ta cố đọc nó (hoặc ngược lại: tệp đã tồn tại mà ta muốn ghi theo chế độ "chỉ tạo nếu chưa có").
- Tệp đang được mở bởi chương trình khác và bị khóa.
- Người dùng không có quyền đọc hoặc ghi tệp hoặc thư mục.
- Đường dẫn tới tệp sai hoặc chứa ký tự bị cấm.
- Hoạt động bị gián đoạn bất ngờ (ví dụ, hết dung lượng ổ đĩa).
- Thói quen xấu: để tệp mở, không đóng stream.
May mắn là .NET cung cấp mọi công cụ để giải quyết mấy vấn đề này. Chúng đơn giản — nhưng cũng giống như dây an toàn, quan trọng là phải thắt chặt.
2. Nguyên tắc cơ bản để làm việc tệp an toàn
Kiểm tra sự tồn tại của tệp và quyền trước
Trước khi đọc tệp, kiểm tra xem nó có tồn tại (ví dụ bằng File.Exists), đặc biệt khi đường dẫn đến từ người dùng:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("Lỗi: không tìm thấy tệp.");
return;
}
Trước khi ghi, đảm bảo thư mục tồn tại và bạn có quyền ghi (hoặc đẩy lỗi lên tầng cao hơn).
Không bao giờ để stream mở
Dùng từ khóa using để tự động đóng stream — đó là best practice:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Ở đây tệp đã được đóng, dù có lỗi xảy ra!
Điều này tránh bị khóa treo và rò rỉ tài nguyên.
Luôn bắt ngoại lệ
Mọi thao tác với tệp đều có thể ném ngoại lệ. Ngay cả khi tệp vừa tồn tại — nó có thể bị xóa/di chuyển ngay khi bạn truy cập. Dùng cấu trúc try-catch:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("Không tìm thấy tệp.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Không có quyền truy cập tệp.");
}
catch (IOException ex)
{
Console.WriteLine($"Lỗi I/O: {ex.Message}");
}
Đừng tin tưởng input từ người dùng
Nếu đường dẫn được nhập bởi người dùng, họ có thể nhập sai (hoặc cố tình làm rơi chương trình). Nên validate đường dẫn (xem Path.GetInvalidPathChars()):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("Đường dẫn không được để trống!");
return;
}
// Thêm: kiểm tra ký tự bị cấm
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("Đường dẫn chứa ký tự không hợp lệ.");
return;
}
// Tiếp theo làm việc an toàn với tệp
}
catch (Exception ex)
{
Console.WriteLine("Lỗi xác thực đường dẫn: " + ex.Message);
}
3. Ví dụ thực hành: đọc tệp "chuyên nghiệp"
Nâng cấp một chút project học tập. Giả sử cần đọc tệp có tên do người dùng nhập và in nội dung ra màn hình. Tất cả kèm xử lý lỗi và tính đến encoding.
Console.Write("Nhập đường dẫn tới tệp: ");
string? path = Console.ReadLine();
// Validate đường dẫn
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("Đường dẫn không được để trống!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("Đường dẫn chứa ký tự không hợp lệ.");
return;
}
// Thử đọc tệp
try
{
if (!File.Exists(path))
{
Console.WriteLine("Không tìm thấy tệp.");
return;
}
// Chỉ định rõ encoding (ví dụ UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("Nội dung tệp:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Không có quyền đọc tệp.");
}
catch (IOException ex)
{
Console.WriteLine("Lỗi I/O: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Lỗi không lường trước: " + ex.Message);
}
Lưu ý mã này sẽ không crash nếu tệp biến mất giữa lúc nhập tên và đọc: ngoại lệ sẽ bị bắt. Stream tự động đóng sau khi ra khỏi khối using — ngay cả khi có lỗi.
4. Ghi tệp: tránh mất dữ liệu
Khi mở tệp để ghi, đặc biệt ở chế độ ghi đè (false là tham số thứ hai của constructor StreamWriter), có nguy cơ vô tình xóa dữ liệu quan trọng. Một vài lưu ý:
Kiểm tra xem có ghi đè tệp hiện có không
Đôi khi nên hỏi người dùng nếu tệp đã tồn tại:
if (File.Exists(path))
{
Console.WriteLine("Cảnh báo: tệp đã tồn tại. Ghi đè? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
Dùng chế độ append khi cần
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": bản ghi mới vào log.");
Thông tin cũ sẽ không biến mất.
5. Bảo vệ khỏi race condition và xung đột
Đôi khi tệp có thể được mở đồng thời bởi nhiều chương trình (ví dụ, client C# của bạn và Notepad++). Điều này có thể gây lỗi. Mặc định StreamReader và StreamWriter dùng chế độ chia sẻ dựa trên FileShare — tức là cho phép hoặc không cho phép người khác đọc.
Có thể điều khiển rõ ràng:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Đọc...
- FileShare.Read: người khác chỉ có thể đọc.
- FileShare.None: không cho phép chia sẻ — tệp là "của bạn".
Nếu cần cho phép vừa đọc vừa ghi bởi các chương trình khác — chọn mode phù hợp, nhưng thận trọng: thường chỉ dùng khi hiểu rõ hậu quả.
6. Ngoại lệ bất ngờ và cách xử lý
Dù theo tất cả hướng dẫn vẫn có thể xảy ra lỗi, ví dụ do thiếu chỗ trên đĩa, USB hỏng hoặc virus. Đây là vài ngoại lệ "kỳ lạ" và cách bắt chúng:
- PathTooLongException — đường dẫn quá dài (hơn 260 ký tự trên các phiên bản Windows cũ).
- DirectoryNotFoundException — thư mục chỉ định không tìm thấy.
- DriveNotFoundException — ví dụ đường dẫn "Z:\\file.txt", mà ổ Z không tồn tại.
- NotSupportedException — ví dụ đường dẫn chứa tổ hợp ký tự không được hỗ trợ.
Nên có khối bắt riêng cho mấy ngoại lệ này — ít nhất log chúng ra file hoặc telemetry.
7. Dùng file tạm để ghi nguyên tử
Vấn đề kinh điển: ghi tệp mà chương trình bị crash giữa chừng — tệp bị lỗi. Các ứng dụng chuyên nghiệp thường dùng chiến lược ghi "nguyên tử":
- Ghi nội dung vào tệp tạm (ví dụ "file.txt.tmp").
- Di chuyển tệp tạm lên vị trí đích (thao tác thường là nguyên tử trên mức hệ thống file) bằng File.Replace.
- Tệp cũ sẽ bị thay hoàn toàn hoặc không bị thay — không có dữ liệu nửa vời.
Ví dụ:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// Ghi tất cả vào tệp tạm
writer.Write(contentForSave);
// Sau khi ghi thành công, thay thế tệp chính
File.Replace(tempPath, path, null); // Di chuyển tệp tạm, thay thế tệp đích (thao tác nguyên tử)
}
catch (Exception ex)
{
Console.WriteLine("Lỗi khi lưu tệp: " + ex.Message);
// Nên xóa tempPath nếu không cần
}
Thực tế nhiều editor và bộ ứng dụng văn phòng làm vậy để đảm bảo tính nhất quán dữ liệu.
8. Dùng các lớp wrapper để truy cập an toàn
.NET có các helper cho thao tác tệp "an toàn". Ví dụ File.ReadAllText và File.WriteAllText tự mở, đọc/ghi và đóng tệp. Nhưng vẫn nên bao trong try-catch:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// Xử lý dữ liệu...
}
catch (Exception ex)
{
Console.WriteLine("Lỗi khi làm việc với tệp: " + ex.Message);
}
Với tệp lớn hãy dùng stream và đọc từng phần, tránh ăn hết bộ nhớ.
GO TO FULL VERSION