Tránh lỗi Null Reference Exception – C# best practices

1

Null reference exception (lỗi tham chiếu null, gọi tắt là lỗi null) là một trong những lỗi phổ biến hàng đầu trong ứng dụng .NET. Tuy nhiên, loại lỗi này lại thường “ẩn mình chờ thời” cho đến khi chạy chương trình.

Các phiên bản của C# lần lượt đưa vào những tính năng mới nhằm hỗ trợ lập trình viên tránh loại lỗi này.

Bài viết này sẽ tổng hợp một số kỹ thuật C# quan trọng giúp bạn viết code tránh lỗi Null reference exception, bao gồm: phép gán null coalescing ??=(C# 8), null- coalescing ?? , null-conditional ?. ?[], Nullable Reference Types (C# 8).

Null Reference Exception

Lỗi Null reference exception (lỗi tham chiếu null, lỗi null) là loại lỗi xuất hiện khi bạn sử dụng một biến mà biến đó không trỏ vào object nào.

Loại lỗi này liên quan đến đặc điểm của kiểu tham chiếu (reference type) trong .NET.

Biến của kiểu tham chiếu (biến tham chiếu) thông thường chứa địa chỉ của một object trong heap. Khi này chúng ta nói rằng “biến trỏ vào object”. Thường người ta cũng gọi lẫn lộn giữa biến và object, tức là dùng từ “biến” để nói về object, và dùng từ “object” khi nói về biến.

Bạn có thể đọc thêm bài học này để nắm rõ sự khác biệt giữa kiểu tham chiếu và kiểu giá trị (value type) trong C# .NET.

.NET cho phép một biến tham chiếu không chứa địa chỉ của object nào. Trong trường hợp này, chúng ta nói rằng biến không trỏ vào object nào, hoặc gọi đây là một null object.

Bất kỳ thao tác nào thực hiện trên null object đều dẫn tới lỗi Null reference exception.

Tuy nhiên, loại lỗi này rất khó chịu ở chỗ, trong giai đoạn viết code, chúng ta thường không lường hết được chúng. Nói cách khác, chúng ẩn rất kỹ khiến ta khó phát hiện ra. Trong khi đó, compiler vẫn dịch code bình thường. Chỉ đến lúc chạy chương trình hoặc test mới có thể phát hiện ra.

Để đảm bảo an toàn trước loại lỗi này, lập trình viên thường phải kiểm tra biến tham chiếu trước khi thực hiện bất kỳ thao tác nào. Tuy nhiên, điều này lại dẫn đến hậu quả là code phình ra và rối rắm vì các kiểm tra điều kiện xuất hiện khắp nơi.

Để hỗ trợ lập trình viên, các phiên bản của C# lần lượt đưa vào những tính năng mới giúp viết code an toàn hơn trước lỗi Null Reference Exception, đồng thời vẫn đảm bảo code ngắn gọn dễ đọc.

Phép toán null-conditional ?. và ?[] (C# 6)

Phép toán null-coditional xuất hiện từ C# 6 và bao gồm hai dạng: ?. áp dụng cho thành viên class, và ?[] áp dụng cho phần tử của mảng/danh sách.

Phép toán ?. cho phép truy xuất thành viên của object nếu object đó khác null.

Phép toán ?[] cho phép truy xuất phần tử của mảng/danh sách nếu mảng/danh sách khác null.

Lấy ví dụ:

string name = null; 
var upper = name.ToUpper();

Khi chạy, đoạn code thứ hai sẽ dính Null Reference Exception ngay lập tức vì name = null. Nếu bạn sửa lại code sử dụng null-conditional ?. như sau:

var upper = name?.ToUpper();

Khi này phương thức ToUpper() sẽ không được thực thi nữa. Chỉ khi nào bạn gán giá trị khác null cho name thì ToUpper() sẽ thực thi. Dĩ nhiên, khi name bằng null thì upper cũng sẽ bằng null.

Phép toán này giúp bạn tránh tình trạng truy xuất thành viên của một null object.

Lấy ví dụ khác, nếu trong class bạn có property

public Action<string> PrintAction { get; set; }

Đây là một delegate property có dạng (string)-> void.

Nếu ở đâu đó bạn gọi PrintAction(“Hello world”), hoàn toàn có khả năng rằng biến PrintAction chưa được gán giá trị nào. Khi đó bạn sẽ dính ngay Null Reference Exception.

Giải pháp an toàn hơn là sử dụng null-conditional opertor ?. để gọi phương thức thành viên Invoke của delegate:

PrintAction?.Invoke("Hello world");

Khi này, nếu vô tình PrintAction vẫn null thì phương thức Invoke sẽ không được gọi. Tức là, Invoke chỉ được gọi nếu PrintAction khác null. Đây là cách thức gọi delegate an toàn được khuyến nghị.

Tóm lại, ?. không khác biệt gì với . (phép toán truy xuất thành viên) nếu object khác null. Nếu object bằng null, phép toán này đơn giản là không làm gì hết. Do vậy, chúng ta tránh được truy xuất thành viên của một null object.

Tương tự, ?[] không khác gì so với [] (phép toán truy xuất phần tử của danh sách) nếu danh sách khác null. Nếu danh sách bằng null (ví dụ, khi bạn quên không khởi tạo danh sách), phép toán ?[] sẽ không làm gì cả. Do đó, bạn tránh truy xuất phần tử của một null list.

Phép toán null-coalescing ?? (C# 7)

Phép toán này có dạng chung như sau:

possibleNullValue ?? valueIfNull

Nếu biểu thức bên trái dấu ?? có giá trị khác null thì kết quả của phép toán chính là giá trị này. Nếu biểu thức bên trái dấu ?? có giá trị null thì kết quả của phép toán là giá trị của biểu thức bên phải dấu ??.

Nếu biểu thức bên trái dấu ?? có giá trị khác null thì biểu thức bên phải dấu ?? sẽ không được tính toán nữa.

Hãy cùng xem ví dụ sau:

Name = name ?? "Donald Trump"; 

Phép toán ?? sử dụng trong ví dụ trên có ý nghĩa: Nếu biến name có giá trị null thì trả lại giá trị “Donald Trump”, nếu name != null thì trả lại giá trị của name. Nói theo kiểu khác, Name sẽ nhận giá trị của name nếu name khác null, và nhận giá trị cố định “Donald Trump” nếu name bằng null. Do đó, Name sẽ không bao giờ bị nhận giá trị null.

Như vậy phép toán ?? giúp chúng ta tránh việc gán giá trị null cho một biến khác.

Thực ra, phép toán null-coalesing giúp chúng ta viết code ngắn gọn xúc tích hơn thôi. Nếu không muốn dùng phép toán này, bạn vẫn có thể viết theo kiểu “truyền thống”:

Name = (name != null) ? name : "Donald Trump";

Hoặc

if (name != null) Name = name; else Name = "Donald Trump";

Phép gán null-coalescing ??= (C# 8)

Đây là phép gán mới xuất hiện trong C# 8. Nó cũng có nghĩa là bạn phải sử dụng .NET Core 3.0, .NET Framework 4.8, hoặc .NET Standard 2.1, trở lên.

Phép gán null-coalescing ??= cho phép bạn viết kết hợp lệnh kiểm tra null và phép gán.

Giả sử bạn có đoạn code bình thường như sau:

public void Method(string value) {
    if (value == null) {
        value = "Batman";
    }
    // Do something with value
}

Giờ bạn có thể viết gộp lại bằng phép gán ??= như sau:

public void Method(string value) {
    value ??= "Batman";
    // Do something with value
}

Như vậy, phép gán value ??= "Batman" được giải nghĩa như sau: nếu value ban đầu có giá trị null thì hãy gán cho nó giá trị mới là “Batman”.

Phép toán này có thể ứng dụng khi thiết lập giá trị mặc định của một biến để đảm bảo nó không null.

Chú ý phân biệt phép toán null-coalescing ?? với phép gán null-coalescing ??=. Phép toán null coalescing ?? trả lại giá trị (cho một biến khác). Phép gán null-coalescing ??= gán giá trị cho chính biến đó.

Nullable Reference Types (C# 8)

Đây cũng là loại kỹ thuật mới nhất xuất hiện trong C# 8.

Tên gọi của loại kỹ thuật này có thể gây khó hiểu. Như bạn đã biết, kiểu tham chiếu (reference type) dĩ nhiên là nullable (biến có thể null)! Tại sao lại còn gọi kỹ thuật mới là nullable reference type nữa.

Trong kỹ thuật này, người ta sử dụng directive (trong code) hoặc điều chỉnh file cấu hình (của dự án) để báo rằng tất cả biến tham chiếu (trong phạm vi ảnh hưởng của directive hoặc toàn dự án) không được phép nhận giá trị null.

Từ đây C# compiler sẽ tự động kiểm tra code để chỉ ra (ngay lúc đang viết code) những nơi vi phạm điều kiện (tức là có thể nhận giá trị null). Qua đó, ngay từ khi viết code bạn đã có thể nhìn ra những lỗi null tiềm tàng và có kế hoạch giải quyết.

Hãy cùng xem một ví dụ đơn giản:

namespace ConsoleApp4 {
    using System.Collections.Generic;
    using System.Linq;
    using static System.Console;
    internal class Program {
        private static void Main() {
            var contex = new Context();
            contex.Add(new Contact { Name = "Donald Trump", Email = "trump@gmail.com" });
            ReadKey();
        }
    }
    internal class Contact {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
    internal class Context {
        public List<Contact> Contacts { get; set; }
        public void Add(Contact contact) => Contacts.Add(contact);
        public Contact Single(int id) => Contacts.Single(c => c.Id == id);
    }
}

Đoạn code nhìn rất bình thường và dường như không có lỗi gì. Tuy nhiên nếu bạn chạy sẽ dính ngay lỗi NullReferenceException:

Vấn đề là property Contacts của lớp Context không được khởi tạo. Contacts mặc định nhận giá trị null. Tuy nhiên, điều này lại hoàn toàn hợp lệ và C# compiler hoạt động bình thường. Chỉ khi chạy chương trình thì vấn đề mới lộ ra. Tình hình cũng xảy ra tương tự với property Name và Email của lớp Contact.

Bây giờ bạn viết directive #nullable
enable

vào đầu file code

Giờ nhìn lại property Contacts bạn sẽ nhận được cảnh báo sớm:

#nullable enble báo hiệu rằng các biến tham chiếu trong cả file code này không được phép null. Nếu biến tham chiếu nào có khả năng nhận giá trị null thì sẽ được cảnh báo sớm ngay từ lúc viết code.

Như vậy, thực chất của kỹ thuật này là cảnh báo sớm về nguy cơ gặp lỗi null ngay từ lúc viết code.

Trong file code bạn có thể sử dụng các directive:

  • #nullable enable – bật chế độ kiểm tra
  • #nullable disable – hủy chế độ kiểm tra
  • #nullable restore – quay trở lại chế độ mặc định

Bạn có thể đặt các directive này ở bất kỳ đâu trong file code.

Bình thường, chế độ mặc định là KHÔNG kiểm tra null. Do đó, việc kiểm tra null chỉ có tác dụng ở khu vực từ #nullable enable đến #nullable restore (hoặc #null disable).

Bạn cũng có thể bật kiểm tra null trên toàn project bằng cách điều chỉnh file csproj như sau:

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
	<LangVersion>8.0</LangVersion>
	<Nullable>enable</Nullable>
</PropertyGroup>

Khi này, chế độ kiểm tra null trở thành chế độ mặc định của dự án. Bạn lại có thể sử dụng #nullable disable để tạm thời hủy kiểm tra (cho đến khi gặp #nullable restore).

Kết luận

Trong bài viết này chúng ta đã điểm qua một số kỹ thuật C# để đối phó với khả năng gây lỗi null (Null Reference Exception), trong đó có những kỹ thuật mới nhất từ C# 8.

Xử lý lỗi null là một vấn đề phức tạp. Trong nhiều trường hợp, các kỹ thuật cũng không thể hoàn toàn giúp bạn. Tuy nhiên, nếu bạn áp dụng tốt các kỹ thuật trên, code của bạn sẽ an toàn và ổn định hơn nhiều.

+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạn!

Theo dõi
Thông báo của
guest

1 Thảo luận
Cũ nhất
Mới nhất
Phản hồi nội tuyến
Xem tất cả bình luận
Cuong

Hay