1. Giới thiệu
Các thao tác với file hầu như luôn phụ thuộc vào yếu tố bên ngoài chương trình: file có thể bị xóa, di chuyển, bị khoá bởi chương trình khác, quyền của người dùng có thể hết hoặc ổ đĩa hết chỗ trống. Tất cả những điều này có thể dẫn tới ngoại lệ — tình huống đặc biệt khiến chương trình bị gián đoạn và một đối tượng ngoại lệ (Exception) được "ném" ra. Nếu bạn không bắt và xử lý ngoại lệ đó, chương trình sẽ kết thúc với lỗi và hiện ra dòng chữ đỏ đáng sợ trong console (hoặc, tệ hơn — rơi xuống trong im lặng).
Trong C# để xử lý những tình huống này ta dùng khối try-catch (gọi nôm na là "bẫy lỗi"). Nó cho phép không chỉ "bắt" lỗi mà còn quyết định tiếp theo phải làm gì: hiển thị thông báo thân thiện, đề nghị chọn file khác, ghi lỗi vào log hoặc thử lại.
Trong ứng dụng thực tế
Nếu bạn viết một máy tính console nhỏ thì có lẽ có thể bỏ qua việc xử lý lỗi file. Nhưng nếu bạn làm phần mềm xử lý tài liệu cho doanh nghiệp hoặc game có chức năng lưu tiến trình — bỏ qua lỗi file giống như đi trên dây mà không có dây an toàn: sớm muộn rồi sẽ có sự cố.
2. Nhắc lại về ngoại lệ
Cú pháp cơ bản của try-catch
Trước khi vào các tình huống cụ thể với file, nhắc lại cú pháp cơ bản của try-catch (mà chúng ta đã gặp trong các bài về ngoại lệ):
try
{
// Ở đây là mã có thể "ném" ngoại lệ
}
catch (Exception ex)
{
// Ở đây ta bắt mọi loại ngoại lệ... Nhưng tốt hơn là đừng làm thế!
Console.WriteLine("Có chuyện không ổn: " + ex.Message);
}
Bên trong try ta đặt mã có khả năng nguy hiểm, còn trong catch — hành động nếu có gì đó sai.
Câu đùa của lập trình viên: "try-catch — giống như chiếc mũ tàng hình cho lỗi. Lỗi vẫn có đó, nhưng bạn không thấy nó!"
Ngoại lệ liên quan tới file
Khi làm việc với file trong .NET, thường gặp các ngoại lệ khi tạo stream, đọc hoặc ghi. Dưới đây là vài kịch bản điển hình và ngoại lệ liên quan:
- Tệp không tìm thấy → FileNotFoundException
- Không có quyền tới tệp/thư mục → UnauthorizedAccessException
- Vấn đề với đường dẫn (ví dụ ký tự không hợp lệ, đường dẫn quá dài) → PathTooLongException, ArgumentException
- Tệp đang được process khác sử dụng → IOException
- Ổ đĩa hết chỗ → IOException
Thường thì các thao tác file liên quan tới các class kế thừa từ IOException. Tất cả chúng đều kế thừa từ kiểu cơ sở System.Exception.
3. Thực hành: bắt và xử lý lỗi khi làm việc với file
Hãy xem các trường hợp điển hình với ví dụ trong "ứng dụng đang phát triển" của chúng ta: giả sử ta cố đọc lời chào từ một file, xử lý và in ra cho người dùng. Ta sẽ thêm kiểm tra lỗi bằng try-catch.
Ví dụ 1: Xử lý "tệp không tìm thấy"
string filePath = "hello.txt";
try
{
string greeting = File.ReadAllText(filePath);
Console.WriteLine("Nội dung tệp: " + greeting);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Không tìm thấy tệp! Kiểm tra xem tệp " + filePath + " có tồn tại không.");
// Có thể in thêm chi tiết
Console.WriteLine("Chi tiết kỹ thuật: " + ex.Message);
}
Nếu tệp "hello.txt" không tồn tại, chương trình sẽ không bị crash mà in thông báo thân thiện. Thế là làm cho chương trình chịu lỗi tốt hơn với "những lộn xộn" của người dùng.
Ví dụ 2: Thiếu quyền truy cập
Ví dụ khó hơn: tệp có nhưng chúng ta không có quyền đọc (ví dụ ai đó chỉ cấp quyền ghi hoặc tệp nằm trong thư mục bảo vệ).
try
{
string secret = File.ReadAllText("C:\\Windows\\System32\\config.txt");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Không có quyền truy cập tệp! Thử chạy chương trình với quyền administrator.");
Console.WriteLine("Lý do kỹ thuật: " + ex.Message);
}
Ví dụ 3: Ngoại lệ chung IOException
Một số lỗi liên quan tới xung đột khoá (ví dụ process khác giữ tệp mở), hết chỗ trên đĩa hoặc vấn đề phần cứng:
try
{
File.WriteAllText("important.txt", "Thông tin quan trọng!");
}
catch (IOException ex)
{
Console.WriteLine("Lỗi khi làm việc với tệp: có thể tệp đang bị chương trình khác sử dụng hoặc ổ đĩa không đủ chỗ.");
Console.WriteLine("Lý do kỹ thuật: " + ex.Message);
}
4. Bắt nhiều ngoại lệ: xử lý theo loại
Đôi khi cần phản ứng khác nhau với các loại ngoại lệ khác nhau. Trong .NET bạn có thể khai báo nhiều khối catch — từ cụ thể tới chung hơn (nếu không, compiler sẽ la lên vì thứ tự sai).
try
{
string content = File.ReadAllText("file.txt");
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Không tìm thấy tệp.");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Không có quyền truy cập tệp!");
}
catch (IOException ex)
{
Console.WriteLine("Lỗi I/O khác: " + ex.Message);
}
Quan trọng: nếu đặt catch (Exception ex) ở đầu, các khối khác sẽ trở nên vô nghĩa vì kiểu cơ sở đã bắt hết!
5. Try-catch lồng nhau và thử lại
Đôi khi bạn không chỉ xử lý lỗi mà còn muốn cho người dùng cơ hội "sửa sai" — ví dụ yêu cầu nhập lại đường dẫn tới tệp:
string filePath;
string content;
int attempts = 0;
const int maxAttempts = 3;
do
{
Console.Write("Nhập đường dẫn tới tệp: ");
filePath = Console.ReadLine();
try
{
content = File.ReadAllText(filePath);
Console.WriteLine("Nội dung tệp:\n" + content);
break;
}
catch (FileNotFoundException)
{
Console.WriteLine("Không tìm thấy tệp! Thử lại nhé.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Không có quyền truy cập tệp! Thử tệp khác.");
}
attempts++;
}
while (attempts < maxAttempts);
if (attempts == maxAttempts)
Console.WriteLine("Quá nhiều lần thử không thành công.");
Mã này là một "giao diện thân thiện" nhỏ cho người dùng hay quên nơi lưu tệp.
6. Lỗi thường gặp và lưu ý: từ lười biếng tới có ý thức
Lỗi phổ biến của người mới (và cả lập trình viên có kinh nghiệm) là bắt mọi ngoại lệ mà không phân biệt, viết đơn giản catch (Exception) và in "Đã xảy ra lỗi!", không quan tâm nguyên nhân. Cách làm đó tệ vì vài lý do. Thứ nhất, nó che giấu bug thực sự trong business logic. Thứ hai, các lỗi không liên quan tới file (ví dụ lỗi gõ hoặc lỗi toán học) có thể bị nuốt mất và việc tìm nguyên nhân thực sự sẽ kéo dài.
Tốt hơn nhiều là chỉ bắt những lỗi bạn biết cách xử lý hợp lý. Nếu không rõ lỗi gì đã xảy ra, tốt hơn để nó "rơi" — tức không bắt: để ứng dụng crash và bạn thấy stack trace, từ đó biết cần sửa chỗ nào.
Lưu ý: Một số ngoại lệ có thể chứa nguyên nhân lồng nhau (InnerException). Thường hữu ích khi phân tích để chẩn đoán chi tiết hơn, đặc biệt khi bạn ghi nhật ký lỗi (log).
Một chi tiết nữa — nếu sau khối catch chương trình không thể tiếp tục (ví dụ không thể mở file cấu hình chính), bạn có thể kết thúc bằng return hoặc thậm chí ném ngoại lệ lại (throw;) để không tạo ra "chương trình-zombie" với nửa chức năng.
GO TO FULL VERSION