Exception Handling & IDisposable
Try-Catch-Finally
Cấu trúc cơ bản
try
{
// Code có thể throw exception
var result = PerformOperation();
}
catch (ArgumentException ex)
{
// Handle specific exception
Log.Error(ex, "Invalid argument");
throw; // Re-throw với original stack trace
}
catch (Exception ex)
{
// Handle general exception
throw new CustomException("Error occurred", ex); // Wrap exception
}
finally
{
// Cleanup - luôn chạy dù có exception hay không
Cleanup();
}
Finally block luôn được thực thi
try
{
Console.WriteLine("In try block");
return;
}
finally
{
Console.WriteLine("In finally block"); // Vẫn được thực thi!
}
// Output:
// In try block
// In finally block
Multiple Catch Blocks
try
{
// Risky operation
}
catch (FileNotFoundException ex)
{
// Handle file not found
Console.WriteLine($"File missing: {ex.FileName}");
}
catch (IOException ex)
{
// Handle IO errors
Console.WriteLine($"IO error: {ex.Message}");
}
catch (Exception ex)
{
// Handle all other exceptions
Console.WriteLine($"Unexpected error: {ex.Message}");
}
Exception Filters (C# 6+)
try
{
// Risky operation
}
catch (Exception ex) when (ex is IOException)
{
// Handle IO exceptions
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
// Handle timeout specifically
}
Anti-patterns cần tránh
// ❌ Bad - Swallow exception
catch (Exception ex)
{
// Do nothing
}
// ✅ Good - Always log or handle
catch (Exception ex)
{
Log.Error(ex);
throw;
}
// ❌ Bad - Lose stack trace
catch (Exception ex)
{
throw ex; // Resets stack trace
}
// ✅ Good - Preserve stack trace
catch (Exception ex)
{
throw; // Preserves stack trace
}
Tại sao throw ex reset stack trace?
Khi sử dụng throw ex, CLR (Common Language Runtime) coi đây là một exception mới được ném từ vị trí hiện tại, thay vì tiếp tục truyền exception gốc lên trên. Điều này dẫn đến việc stack trace bị reset và chỉ bắt đầu từ vị trí throw ex.
Cơ chế hoạt động
void MethodA()
{
MethodB();
}
void MethodB()
{
MethodC();
}
void MethodC()
{
throw new InvalidOperationException("Error in C");
}
try
{
MethodA();
}
catch (Exception ex)
{
// ❌ throw ex - Stack trace bị reset
throw ex;
// Stack trace chỉ còn:
// at Program.Main() in Program.cs:line XX
// (Mất thông tin MethodA -> MethodB -> MethodC)
}
try
{
MethodA();
}
catch (Exception ex)
{
// ✅ throw - Stack trace được giữ nguyên
throw;
// Stack trace đầy đủ:
// at Program.MethodC() in Program.cs:line XX
// at Program.MethodB() in Program.cs:line XX
// at Program.MethodA() in Program.cs:line XX
// at Program.Main() in Program.cs:line XX
}
Giải thích chi tiết
| Aspect | throw ex | throw |
|---|---|---|
| Stack trace | Bị reset từ vị trí throw | Giữ nguyên stack trace gốc |
| Exception object | Tạo mới (về mặt stack trace) | Giữ nguyên object gốc |
| Debugging | Khó tìm root cause | Dễ dàng trace ngược lại |
| IL Code | throw instruction | rethrow instruction |
Khi nào dùng throw ex?
Thực tế, hầu như không nên dùng throw ex. Tuy nhiên, có một số trường hợp đặc biệt:
// Trường hợp duy nhất chấp nhận được:
// Khi bạn muốn wrap exception vào một exception khác
catch (Exception ex)
{
throw new CustomException("Context-specific message", ex);
// InnerException giữ nguyên stack trace gốc
}
ExceptionDispatchInfo - Giữ stack trace khi re-throw
Nếu bạn cần re-throw exception ở một vị trí khác (ví dụ: trong async scenarios), sử dụng ExceptionDispatchInfo:
using System.Runtime.ExceptionServices;
ExceptionDispatchInfo capturedException = null;
try
{
// Some operation
}
catch (Exception ex)
{
capturedException = ExceptionDispatchInfo.Capture(ex);
}
// Later, possibly in a different location
capturedException?.Throw();
// Stack trace vẫn được giữ nguyên!
Rule of thumb: Luôn sử dụng
throw;thay vìthrow ex;để giữ nguyên stack trace gốc, giúp việc debugging dễ dàng hơn.
IDisposable Interface
Interface Definition
public interface IDisposable
{
void Dispose();
}
Implementing IDisposable
public class ResourceHolder : IDisposable
{
private bool _disposed = false;
private FileStream _fileStream;
public ResourceHolder(string path)
{
_fileStream = new FileStream(path, FileMode.Open);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_fileStream?.Dispose();
}
// Free unmanaged resources
// (if any)
_disposed = true;
}
}
~ResourceHolder()
{
Dispose(false);
}
}
Using Statement
// Traditional using statement
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// Use stream
} // Dispose() called automatically
// C# 8+ using declaration
using var stream = new FileStream("file.txt", FileMode.Open);
// Use stream
// Dispose() called at end of scope
Tương tác giữa Try-Catch-Finally và IDisposable
Using vs Try-Finally
// Using statement (recommended)
using var resource = new ResourceHolder("file.txt");
resource.DoWork();
// Dispose() called automatically, even if exception occurs
// Equivalent try-finally
ResourceHolder resource = null;
try
{
resource = new ResourceHolder("file.txt");
resource.DoWork();
}
finally
{
resource?.Dispose();
}
Multiple Resources với Using
// Multiple using statements
using var file1 = new FileStream("file1.txt", FileMode.Open);
using var file2 = new FileStream("file2.txt", FileMode.Open);
using var reader = new StreamReader(file1);
using var writer = new StreamWriter(file2);
// All resources disposed in reverse order
Exception trong Dispose
public class SafeResource : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
try
{
Dispose(true);
}
catch
{
// Log but don't throw - Dispose should not throw
Log.Error("Error during dispose");
}
finally
{
GC.SuppressFinalize(this);
_disposed = true;
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Cleanup managed resources
}
}
}
Best Practices
-
Luôn sử dụng
usingcho IDisposable resources// ✅ Good using var connection = new SqlConnection(connectionString); connection.Open(); // ❌ Bad var connection = new SqlConnection(connectionString); connection.Open(); // Forgot to dispose! -
Không throw exception từ Dispose
public void Dispose() { try { Cleanup(); } catch (Exception ex) { Log.Error(ex); // Log, don't throw } } -
Implement Dispose pattern đúng cách
public class MyResource : IDisposable { private bool _disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { // Dispose managed resources } // Free unmanaged resources _disposed = true; } } } -
Kết hợp try-catch với using
try { using var resource = new ResourceHolder("file.txt"); resource.DoWork(); } catch (IOException ex) { Log.Error(ex, "IO error during operation"); throw; } // resource.Dispose() called automatically
Khi nào implement IDisposable?
- Khi class sử dụng unmanaged resources (file handles, database connections, etc.)
- Khi class chứa các IDisposable members
- Khi cần deterministic cleanup
Khi nào KHÔNG cần IDisposable?
- Chỉ sử dụng managed resources
- Không cần cleanup đặc biệt
- Object lifetime ngắn và GC có thể handle