Vì sao NullReferenceException vẫn là nỗi ám ảnh của dev .NET
Chắc bạn đã từng chạy thử chương trình và thấy dòng chữ “Object reference not set to an instance of an object” đỏ lòm hiện ra. Đó chính là NullReferenceException, lỗi quen mặt nhất với bất kỳ ai làm C#. Trong đội ngũ của chúng tôi, không ít lần cả nhóm phải ngồi xem một stack trace chỉ vì một biến nào đó chưa được gán giá trị.
Nguyên nhân thì không phức tạp. Bạn đang truy cập thuộc tính hoặc gọi phương thức trên một đối tượng đang mang giá trị null. Runtime không biết lấy dữ liệu ở đâu, nên nó dừng lại và ném exception. Trong ứng dụng web, hậu quả hiếm khi chỉ dừng ở một dòng log. Người dùng có thể nhận về trang lỗi 500 thay vì nội dung họ cần, và lúc đó việc truy vết nguyên nhân tốn kha khá thời gian.
Cách xử lý quen thuộc nhất là kiểm tra null trước mỗi lần truy cập. Một hai dòng if thì chẳng có gì đáng ngại. Nhưng khi chuỗi truy cập dài ra, code bắt đầu lồng nhau, khó đọc và dễ sót một trường hợp. Chính những đoạn kiểm tra null ấy lại là nơi phát sinh nhiều lỗi nhất trong các dự án chúng tôi từng làm.
May mắn là C# đã có sẵn một cú pháp gọn hơn: null-conditional operator. Phần dưới đây sẽ đi qua cách nó hoạt động, kèm vài ví dụ bạn có thể áp dụng ngay vào dự án của mình.
Null-conditional operator hoạt động thế nào
Toán tử này được viết bằng ?.. Bạn đặt dấu hỏi chấm ngay trước dấu chấm khi truy cập thành viên của một đối tượng có thể là null. Nếu đối tượng đó là null, biểu thức trả về null thay vì ném exception.
Hãy so sánh hai cách viết sau:
if (khachHang != null)
{
var ten = khachHang.Ten;
}
và:
var ten = khachHang?.Ten;
Hai đoạn này cho kết quả tương đương: nếu khachHang là null thì ten nhận giá trị null. Khác biệt nằm ở độ gọn gàng. Có thể hình dung toán tử này như một người gác cổng: đối tượng còn đó thì cho đi tiếp, còn không thì trả về giá trị rỗng mà không gây lỗi.
Cú pháp này phát huy rõ nhất khi làm việc với đối tượng lồng nhau. Ví dụ donHang?.NguoiMua?.DiaChi?.ThanhPho: chỉ cần một mắt xích trong chuỗi là null, toàn bộ biểu thức trả về null. Bạn không phải viết ba tầng if lồng vào nhau.
Lưu ý: khi kết quả là null, bạn không biết chính xác mắt xích nào bị thiếu. Nếu đang cần chẩn đoán lỗi, hãy tách chuỗi ra và kiểm tra từng bước. Với phần lớn trường hợp hiển thị hay xử lý dữ liệu, biết kết quả cuối là null là đủ để quyết định bước tiếp theo.
Ứng dụng trong code thực tế
Truy cập thuộc tính lồng nhau
Giả sử KhachHang có thuộc tính DonHang, và DonHang lại có thuộc tính SanPham. Bạn muốn lấy tên sản phẩm. Cách viết cũ thường trông như sau:
string tenSanPham = null;
if (khachHang != null)
{
if (khachHang.DonHang != null)
{
if (khachHang.DonHang.SanPham != null)
{
tenSanPham = khachHang.DonHang.SanPham.Ten;
}
}
}
Với null-conditional, cùng logic chỉ còn đúng một dòng:
string tenSanPham = khachHang?.DonHang?.SanPham?.Ten;
Khi làm việc với Entity Framework, kiểu code này xuất hiện khá thường xuyên, vì quan hệ giữa các bảng có thể thiếu dữ liệu ở một đầu nào đó. Tuy vậy, đừng lạm dụng. Một chuỗi năm, sáu cấp ?. liên tiếp sẽ khiến người đọc phải dừng lại để giải mã. Quy tắc đơn giản là giữ tối đa khoảng ba đến bốn cấp trong một biểu thức, phần còn lại tách ra biến trung gian có tên rõ ràng.
Kết hợp với toán tử gộp null để có giá trị mặc định
Null-conditional thường đi kèm toán tử ??, cho phép chỉ định giá trị thay thế khi vế trái là null:
string tenThanhPho = khachHang?.DiaChi?.ThanhPho ?? "Chưa cập nhật";
Một dòng này thay thế được cả khối if-else. Trong các API trả về JSON, cách làm này giúp hợp đồng API rõ ràng hơn, và phía giao diện không phải viết thêm điều kiện kiểm tra.
Lưu ý khi làm việc với kiểu giá trị: null-conditional không tự chuyển kiểu cho bạn. Nếu gọi khachHang?.Tuoi mà Tuoi là int, kết quả sẽ là int?. Bạn cần xử lý null trước khi gán vào biến kiểu int, chẳng hạn dùng ?? 0, nếu không trình biên dịch sẽ báo lỗi.
Gọi phương thức an toàn
Toán tử này cũng dùng được với phương thức. Biểu thức khachHang?.TinhTongTien() sẽ không chạy phương thức nếu khachHang là null, mà trả về null. Với phương thức trả về kiểu giá trị, kiểu kết quả được nâng thành nullable, nên bạn cần kiểm tra trước khi sử dụng. Nếu muốn hiểu sâu hơn cách viết truy vấn gọn gàng, hãy xem thêm bảy kinh nghiệm thực tế về LINQ trong C# và .NET.
Những sai lầm chúng tôi từng mắc
Khi mới áp dụng, đội ngũ cũng vấp vài lỗi nhỏ. Lỗi đầu tiên là dùng null-conditional để che đi một thiết kế có vấn đề. Nếu một thuộc tính gần như lúc nào cũng null, có lẽ nó nên được khởi tạo ngay từ đầu, hoặc nên cân nhắc pattern Null Object. Toán tử này phù hợp nhất với dữ liệu thực sự không chắc chắn, như dữ liệu người dùng nhập vào hay kết quả truy vấn tùy chọn.
Lỗi thứ hai liên quan đến event. Cách an toàn quen thuộc là SuKien?.Invoke(this, e);. Trước đây có đoạn code kiểm tra null rồi mới gọi, vừa dài dòng vừa dễ quên mỗi khi thêm handler mới. Dùng ?.Invoke gọn hơn và giảm rủi ro sót.
Kết luận
Null-conditional operator là một trong những mẹo C# ngắn gọn đáng áp dụng sớm nhất. Nó giúp code gọn hơn, bớt các khối kiểm tra null thừa và hạn chế NullReferenceException ở những chỗ dữ liệu có thể thiếu. Bạn nên bắt đầu từ việc truy cập thuộc tính của đối tượng dữ liệu, sau đó kết hợp với ?? để đặt giá trị mặc định.
Nếu bạn đang làm việc với dữ liệu quan hệ, hãy xem thêm các sai lầm thường gặp khi dùng Entity Framework để tránh những lỗi tương tự từ tầng truy cập dữ liệu. Việc xử lý exception có hệ thống cũng quan trọng không kém, và bạn có thể đọc thêm cách xử lý exception đúng cách thay vì try-catch tràn lan.
Tham khảo thêm tài liệu chính thức về null-conditional operators trên Microsoft Learn để nắm đầy đủ các trường hợp sử dụng.


