1. Giới thiệu: tại sao file hay "tạo vấn đề"?
Không ít lần bạn mở một tài liệu — rồi hệ thống thông báo: "File không tìm thấy". Hoặc cố lưu cái gì đó rồi nhận được: "Từ chối truy cập". Đó chính là những tình huống mà file hành xử lạ. Nếu với encoding chúng ta gặp "kí tự lạ", thì ở đây vấn đề liên quan tới hệ thống file.
Bạn có thể tưởng tượng hệ thống file như một thư viện lớn, còn ứng dụng là thủ thư. Khi ứng dụng yêu cầu một file, có thể nhận được nhiều phản hồi khác nhau:
- Sách (tức file) không tồn tại — nó đơn giản là không có.
- Phần của thư viện nơi cuốn sách phải ở cũng không có — thư mục cần thiết bị thiếu.
- Cuốn sách "bị khoá" hoặc đang được người khác mượn — file đang được process khác dùng hoặc không có quyền truy cập.
- Kệ đã đầy — không đủ chỗ trên đĩa để tạo file mới.
- Hoặc, ví dụ, cố đặt sách vào mục trả DVD — tức thực hiện thao tác không được hỗ trợ.
Trong C# tất cả những tình huống này được biểu diễn bằng ngoại lệ. Nhiệm vụ của developer — không phải để chương trình "rụng" mà là dự đoán các vấn đề có thể xảy ra và xử lý chúng gọn gàng. Chẳng ai muốn người dùng nhìn thấy thông báo lỗi bí ẩn với đống chữ khó hiểu.
Chúng ta đã biết cấu trúc try-catch. Đó là phao cứu sinh, cho phép "bắt" ngoại lệ và hành động thay vì để chương trình bị crash.
// Đây là người bạn cũ, nhắc lại từ Bài giảng 57
try
{
// Ở đây viết code có thể gây lỗi
// Ví dụ, cố đọc file
}
catch (Exception ex) // Bắt mọi ngoại lệ
{
// Ở đây xử lý lỗi
Console.WriteLine($"Ối, đã xảy ra lỗi: {ex.Message}");
}
Hôm nay chúng ta sẽ đi sâu vào các ngoại lệ đặc thù khi làm việc với file. Điều này giúp viết code đáng tin cậy hơn, biết "nói chuyện" với hệ thống file ngay cả khi nó "tâm trạng thất thường".
Ngoại lệ — không phải bug, mà là tín hiệu báo hoạn!
Cần hiểu rằng: ngoại lệ không luôn là lỗi trong code của bạn. Thường đó là tín hiệu có điều gì đó không ổn ở môi trường bên ngoài mà code tương tác. Hệ thống file là ví dụ rõ ràng. Bạn có thể viết đúng phép đọc file, nhưng nếu user xóa file trước khi chương trình đọc, bạn sẽ nhận ngoại lệ. Và điều đó bình thường! Nhiệm vụ của bạn là dạy chương trình phản hồi các tình huống đó.
Hãy xem qua những "tín hiệu báo hoạn" phổ biến khi làm việc với file.
2. FileNotFoundException: file đã không tồn tại
Đây có lẽ là ngoại lệ phổ biến nhất khi làm việc với file. Nó xảy ra khi bạn cố mở, đọc hoặc thực hiện thao tác khác trên file không tồn tại ở đường dẫn chỉ định.
Ví dụ đời thường: Bạn nhờ bạn mang cuốn sách "Lập trình C# 14 cho người mới" từ thư viện, nhưng họ đáp: "Không có cuốn đó". Tương tự chương trình có thể hỏi OS: "Cho tôi file settings.txt", mà hệ thống trả lời: "Xin lỗi, không có".
Hãy viết đoạn code đọc file abracadabra.txt cho ứng dụng quản lý tác vụ. Nếu file không có, ta nên báo cho user thay vì để chương trình rơi.
try
{
using var reader = new StreamReader("abracadabra.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
Console.WriteLine("File không tìm thấy: " + ex.FileName);
}
Trong đời thường giống như ra bến xe mà xe không đến — buồn rười rượi.
Ghi chú: Thường ngoại lệ này kèm theo đường dẫn sai (ví dụ bạn quên là đang chạy từ thư mục khác).
3. DirectoryNotFoundException: thư mục bốc hơi
Ngoại lệ này khá giống FileNotFoundException, nhưng liên quan tới thư mục, chứ không phải file. Nếu bạn chỉ định đường dẫn như "C:\MyDocuments\MyProject\Data\report.txt", mà thư mục Data không tồn tại, bạn sẽ nhận DirectoryNotFoundException.
Trong app của chúng ta, nếu muốn lưu settings vào thư mục con data, ví dụ "./data/app_settings.txt", và thư mục đó không có, khi ghi hoặc đọc file bạn sẽ gặp vấn đề này.
DirectoryNotFoundException có thể bắt riêng, giống FileNotFoundException, hoặc là một phần của IOException tổng quát mà ta sẽ nói đến sau.
try
{
using var writer = new StreamWriter(@"C:\very\strange\path\file.txt");
writer.WriteLine("Hello world");
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine("Không tìm thấy thư mục!");
}
Lỗi thường gặp: Thư mục có thể bị xóa bất ngờ (ví dụ ai đó dọn file tạm), hoặc bạn cấu hình sai đường dẫn để ghi.
4. UnauthorizedAccessException: cấm vào!
Tưởng tượng bạn muốn đặt sách lên kệ, nhưng có biển "Chỉ nhân viên được phép". Đó chính là UnauthorizedAccessException! Nó xảy ra khi chương trình của bạn không có quyền cần thiết với file hoặc thư mục. Nguyên nhân có thể là:
- Quyền user không đủ: Bạn cố ghi vào thư mục chỉ admin mới được ghi (ví dụ C:\Windows).
- File đánh dấu chỉ đọc: Bạn cố sửa file có thuộc tính "read-only".
- File là hệ thống hoặc ẩn: Có những hạn chế đặc biệt với nó.
Vấn đề này phổ biến trong môi trường doanh nghiệp hoặc khi người dùng cài app vào thư mục được bảo vệ.
Thử ghi file vào thư mục hệ thống mà user bình thường không có quyền. (Chú ý: chạy code này cẩn thận để không làm bừa hệ thống hoặc chạy trong sandbox).
try
{
using var writer = new StreamWriter("/system/settings.conf");
writer.WriteLine("Toàn quyền thuộc về sinh viên!");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Không có quyền truy cập file hoặc thư mục!");
}
Nếu chạy code này không phải admin, bạn rất có thể thấy thông báo "Truy cập thư mục này bị từ chối". Nếu chạy dưới quyền admin thì file có thể được tạo. Ví dụ cho thấy tầm quan trọng của việc xử lý UnauthorizedAccessException để user hiểu vì sao thao tác thất bại.
5. IOException: "ôi thôi" chung của I/O
IOException là ngoại lệ tổng quát liên quan tới các thao tác I/O. Nó được ném khi có vấn đề với thiết bị I/O hoặc hệ thống file mà không rơi vào các ngoại lệ cụ thể hơn như FileNotFoundException hay UnauthorizedAccessException.
Những kịch bản hay gặp khi nhận IOException:
- File đang được chương trình khác dùng: Ví dụ bạn cố xóa file đang mở trong Notepad hoặc program khác.
- Đĩa đầy: Không đủ dung lượng để ghi file.
- File hoặc hệ thống file bị hỏng: Ít gặp nhưng có thể xảy ra.
- Sự cố mạng: Nếu file nằm trên ổ mạng và kết nối bị mất.
- Tên file hoặc thư mục quá dài. (Có thể là PathTooLongException, nhưng đôi khi rơi vào IOException).
IOException giống như "chìa khoá vạn năng" cho nhiều vấn đề. Khi bắt IOException, nên xem thuộc tính Message để biết chi tiết hơn.
try
{
using var file = new FileStream("busyfile.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
// giữ file mở bằng cách nào đó
// đồng thời ở nơi khác:
using var writer = new StreamWriter("busyfile.txt");
writer.WriteLine("Cố gắng ghi...");
}
catch (IOException ex)
{
Console.WriteLine("Lỗi nhập xuất: " + ex.Message);
}
Điểm quan trọng: IOException là lớp cơ sở cho nhiều ngoại lệ liên quan file khác.
6. Những "rắc rối" khác cũng quan trọng
PathTooLongException
Đây ít gặp hơn nhưng vẫn xảy ra: đường dẫn (hoặc tên file/thư mục) quá dài cho OS. Ví dụ bạn đặt tên file dài như tóm tắt "Chiến tranh và hòa bình", Windows có thể không chịu.
Trên Windows giới hạn lịch sử là 260 ký tự cho toàn bộ đường dẫn. Các phiên bản mới hơn của OS và .NET cho phép "long paths", nhưng không phải lúc nào cũng bật sẵn.
try
{
string veryLongPath = new string('a', 300); // 300 ký tự!
using var writer = new StreamWriter(veryLongPath + ".txt");
writer.WriteLine("Tên file quá dài!");
}
catch (PathTooLongException ex)
{
Console.WriteLine("Tên file hoặc đường dẫn quá dài!");
}
NotSupportedException
Trường hợp hiếm nhưng "khó tin" khi bạn truyền vào constructor của StreamReader hoặc FileStream một chuỗi đường dẫn không hợp lệ, ví dụ chứa ký tự cấm hoặc dạng "ma thuật" như C:::\wow???\file.txt.
7. Những mẹo hữu ích
Ngoại lệ chịu trách nhiệm cho điều gì
| Ngoại lệ | Nguyên nhân | Tình huống ví dụ |
|---|---|---|
|
File không tìm thấy | Mở file không tồn tại |
|
Thư mục không tìm thấy | Mở file trong thư mục đã bị xóa |
|
Không có quyền | Ghi vào thư mục được bảo vệ |
|
Lỗi I/O chung | File đang được process khác giữ |
|
Đường dẫn quá dài | Tên file/thư mục quá dài |
|
Định dạng đường dẫn không hợp lệ | Đường dẫn chứa ký tự cấm |
Lỗi thường gặp và các trường hợp đặc biệt
Ví dụ: File đang được process khác dùng
Tưởng tượng bạn mở file text trong Notepad và quên đóng. Lúc đó chương trình của bạn cố ghi vào file đó. Đây là lúc bạn gặp IOException (hoặc "sharing violation").
Ví dụ: Thiếu quyền truy cập
Thử lưu vào C:\Windows mà không có quyền admin — bạn sẽ nhận UnauthorizedAccessException. Tương tự nếu mở file chỉ để đọc mà cố ghi vào sẽ gặp vấn đề.
Ví dụ: Đường dẫn không hợp lệ
Trên Windows không được dùng ký tự <>:"/\|?* trong tên file. Nếu cố ghi file như vậy — chương trình sẽ ném NotSupportedException (hoặc ArgumentException).
Ví dụ: Đĩa đầy
Thú vị là điều này cũng ném IOException — ví dụ khi đĩa hết chỗ (đó là lý do thỉnh thoảng nên dọn thư mục Downloads).
Cách tránh rơi vào bẫy: best practices phát hiện lỗi
- Kiểm tra sự tồn tại file bằng File.Exists và Directory.Exists trước khi mở file. Nhưng cẩn thận: file có thể biến mất hoặc xuất hiện ngay sau kiểm tra (race condition cổ điển).
- Đừng bao giờ "nuốt" ngoại lệ hoàn toàn (đừng chỉ làm catch { }), trừ khi bạn đang viết logger nghiêm túc. Luôn ít nhất là log hoặc hiển thị cho user biết chuyện gì đã xảy ra.
- Cố gắng bắt các ngoại lệ cụ thể (FileNotFoundException, DirectoryNotFoundException) thay vì chỉ bắt Exception chung.
- Với app đa nền tảng, nhớ rằng quyền, định dạng đường dẫn, độ dài tên file khác nhau trên Windows, Linux, macOS.
- Nếu xử lý file văn bản, luôn chỉ định rõ encoding — tránh bất ngờ.
- Với thao tác file hàng loạt, xử lý theo kiểu "bulk" và chuẩn bị cho lỗi ở từng bước.
Bảng tóm tắt các ngoại lệ điển hình
| Ngoại lệ | Khi nào xảy ra? | Cách phòng/ xử lý? |
|---|---|---|
|
Không có file ở đường dẫn chỉ định | Kiểm tra bằng File.Exists hoặc tạo file |
|
Đường dẫn chứa thư mục không tồn tại | Kiểm tra đường dẫn, tạo thư mục bằng Directory.CreateDirectory |
|
Không có quyền trên file/thư mục, file chỉ đọc, bị process khác chiếm | Chạy dưới user phù hợp, kiểm tra ACL, đóng file đúng cách |
|
Lỗi I/O chung, file bị chiếm, đĩa đầy | Dùng try-catch, hạn chế giữ file mở |
|
Đường dẫn hoặc tên file quá dài | Rút ngắn đường dẫn, dùng đường dẫn tương đối |
|
Định dạng đường dẫn không hợp lệ | Kiểm tra chuỗi đường dẫn tránh ký tự cấm |
Sơ đồ khối "Phải làm gì khi file lỗi?"
flowchart TD
A[Thao tác với file] --> B{Có ném ngoại lệ?}
B -- Không --> C[Thao tác hoàn tất thành công]
B -- Có --> D{Loại ngoại lệ?}
D -- FileNotFound --> E[Yêu cầu user chỉ file đúng hoặc tạo file]
D -- DirectoryNotFound --> F[Tạo thư mục thiếu]
D -- UnauthorizedAccess --> G[Yêu cầu user chạy lại với quyền phù hợp]
D -- IOException --> H[Kiểm tra ai đang giữ file, kiểm tra ổ đĩa]
D -- PathTooLong --> I[Rút ngắn đường dẫn]
D -- NotSupported --> J[Kiểm tra định dạng đường dẫn]
D -- Other --> K[Hiển thị thông báo và kiểm tra logs]
8. Các ngoại lệ điển hình trông thế nào trong app thực tế?
Phát triển project mẫu, giả sử ta có mini-app lưu ghi chú của user vào file, rồi đọc lại.
string notesPath = "notes.txt";
Console.Write("Nhập ghi chú: ");
string note = Console.ReadLine();
try
{
// Lưu ghi chú
using var writer = new StreamWriter(notesPath, true, Encoding.UTF8);
writer.WriteLine(note);
// Đọc tất cả ghi chú
Console.WriteLine("Ghi chú của bạn:");
using var reader = new StreamReader(notesPath, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
Console.WriteLine("File ghi chú không tìm thấy. Thử tạo nó thủ công.");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("Đường dẫn tới file ghi chú sai. Kiểm tra xem thư mục có tồn tại không.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Không có quyền ghi hoặc đọc file. Hãy chạy chương trình với quyền admin.");
}
catch (IOException ex)
{
Console.WriteLine("Đã xảy ra lỗi I/O: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Lỗi bất ngờ: " + ex.Message);
}
Ví dụ này minh hoạ tình huống điển hình: cố lưu dữ liệu vào file rồi đọc lại. Mỗi khối catch xử lý một loại lỗi — từ thiếu file/thư mục tới vấn đề quyền và lỗi I/O chung. Nhờ đó chương trình không "té" ngay khi có sự cố, mà báo cho user biết chuyện gì xảy ra và có thể làm gì. Cách tiếp cận này làm app đáng tin hơn và thân thiện với người dùng.
GO TO FULL VERSION