1. Giới thiệu
Hãy tưởng tượng code giống như một vở kịch, còn biến thì là diễn viên. Mỗi lần biến xuất hiện ở một cảnh mới (block code), nó có thể là "sống" (có giá trị) hoặc là "ma" (null). Nhưng vấn đề là: khán giả — trình biên dịch — không xem trực tiếp mà chỉ đọc kịch bản. Nó thấy chỗ nào diễn viên được giao vai (gán giá trị), chỗ nào biến mất (trở thành null). Nhưng nó không thể nhìn vào thực tế (chạy chương trình) — chỉ đoán dựa trên kịch bản thôi!
Static flow analysis là quá trình mà trình biên dịch kiểm tra code trước khi chạy chương trình, để xem có ai đó chuẩn bị diễn cảnh "ngã vì NullReferenceException" không.
Bản chất của quá trình
Từ C# 8.0 và khi có chế độ Nullable Reference Types (NRT), trình biên dịch sẽ phân tích xem biến có thể thành null ở nhánh nào không. Nếu có — nó báo liền. Nó sẽ không cho bạn "dùng tên diễn viên mà tự nhiên biến thành ma".
2. Cách đọc cảnh báo của trình biên dịch
Các loại cảnh báo chính
Khi bật chế độ NRT, trình biên dịch đặc biệt chú ý tới biến tham chiếu (string, User, mảng, object). Đây là mấy loại cảnh báo phổ biến nhất:
- Possible null assignment — bạn đang cố gán giá trị có thể là null cho biến không cho phép null.
- Dereference of a possibly null reference — bạn truy cập thành viên của object, nhưng trình biên dịch nghi ngờ nó có thể là null.
- Possible null return — method có thể trả về null, dù theo signature thì phải trả về giá trị.
- Nullable object must have a value — bạn dùng .Value với nullable-type mà chưa kiểm tra.
- Unassigned non-nullable property — bạn chưa gán giá trị cho field/property không cho phép null.
Thường thì trong IDE (ví dụ Rider hoặc Visual Studio) mấy cảnh báo này sẽ được gạch sóng, di chuột vào sẽ thấy giải thích.
Ví dụ cảnh báo điển hình
#nullable enable
string? possibleNull = GetString();
// Ôi trời, trình biên dịch la lên: "Dereference of a possibly null reference"
int length = possibleNull.Length; // Cảnh báo!
Trình biên dịch không chắc possibleNull khác null, nên truy cập .Length có thể gây exception.
Nếu method như này:
string? GetString() { ... }
Và bạn viết:
string nonNullable = GetString();
Thì sẽ bị cảnh báo: có thể truyền null không an toàn vào biến non-nullable.
3. Trình biên dịch phân tích code của bạn
Ví dụ 1: Gán thẳng
string? name = null;
Console.WriteLine(name.Length); // Cảnh báo: có thể truy cập null
Ở đây quá rõ: name là null, nên truy cập property là nguy hiểm.
Ví dụ 2: Kiểm tra null
string? name = GetUserName();
if (name != null)
{
Console.WriteLine(name.Length); // Ok luôn!
}
else
{
Console.WriteLine("Tên chưa nhập!");
}
Trình biên dịch hiểu rằng trong block if (name != null) thì biến an toàn. Đó chính là flow analysis — phân tích luồng giá trị theo điều kiện.
4. Trình biên dịch không thể "nhìn vào trong method"
Nó chỉ nhìn signature — nếu ghi là method trả về string?, nó tin là có thể trả về null, dù bên trong luôn trả về string.
Ví dụ 3: Trả về giá trị từ method
string? GetMaybeName(bool useName)
{
if (useName)
return "Code Jedi";
else
return null;
}
void PrintLength()
{
string? name = GetMaybeName(false);
Console.WriteLine(name.Length); // Cảnh báo!
}
Trình biên dịch thấy GetMaybeName có thể trả về null, nên nhắc bạn cẩn thận.
5. Các tình huống và cảnh báo phổ biến
Gán nullable vào biến non-nullable
string? maybeUser = GetUser();
string alwaysUser = maybeUser; // warning: possible null assignment
Để hết cảnh báo:
string alwaysUser = maybeUser ?? "Khách";
Giờ lúc nào cũng có string — hoặc từ method, hoặc là "Khách".
Quên kiểm tra .HasValue với nullable value-type
int? age = GetAge();
int realAge = age.Value; // warning: có thể truy cập null
Nên làm như này:
if (age.HasValue)
Console.WriteLine(age.Value);
Hoặc:
int realAge = age ?? -1;
Auto-property chưa khởi tạo
class User
{
public string Name { get; set; } // warning: chưa khởi tạo!
}
Cách giải quyết:
public string Name { get; set; } = "Không tên";
hoặc
public string? Name { get; set; }
6. Trường hợp phân tích luồng phức tạp
Phân tích trong vòng lặp
string? text = null;
while (text == null)
{
text = Console.ReadLine();
}
Console.WriteLine(text.Length); // ok: trình biên dịch hiểu rồi!
Nhiều nhánh gán giá trị
string? name;
if (Random.Shared.Next() % 2 == 0)
name = "Alice";
else
name = null;
Console.WriteLine(name.Length); // warning: possible null dereference
Trình biên dịch phân tích tất cả các nhánh. Nếu có nhánh nào là null — nó sẽ cảnh báo.
7. Làm sao "dỗ" trình biên dịch (và khi nào không nên làm vậy)
Đôi khi bạn chắc chắn giá trị không thể là null, nhưng trình biên dịch không tin:
string? value = GetSomething();
if (value == null)
throw new Exception("Cần có giá trị!");
Console.WriteLine(value.Length); // Cảnh báo biến mất
Hoặc bạn dùng toán tử !:
string? value = GetSomething();
Console.WriteLine(value!.Length); // Không cảnh báo, nhưng nguy hiểm
Cái này gọi là null-forgiving operator. Nó không kiểm tra gì hết — chỉ bảo trình biên dịch: "bình tĩnh, mọi thứ trong tầm kiểm soát". Nhưng nếu bạn sai — lúc chạy sẽ dính exception.
GO TO FULL VERSION