null nghĩa là "không có giá trị". Kiểu tham chiếu (lớp, chuỗi, mảng) từ trước đến nay luôn có thể chứa null. Kiểu giá trị (int, decimal, bool, DateTime, struct) thì không, trừ khi bạn biến chúng thành nullable bằng ?. Khi đó C# cho bạn một nhóm toán tử nhỏ để làm việc với giá trị có thể bị thiếu: ??, ??= và ?..
Kiểu giá trị nullable: int?
Thêm ? sau một kiểu giá trị để cho phép null. int? là cách viết tắt của struct Nullable<int>, chứa một giá trị cùng một cờ cho biết giá trị đó có tồn tại hay không:
Đầu ra:
False
True
True
5.50
0
3
Rated 4 stars
4
Kiểu giá trị nullable thường đến từ các trường form không bắt buộc, tham số truy vấn và các cột NULL trong cơ sở dữ liệu: một Order có DateTime? ShippedAt là chưa giao hàng chừng nào giá trị còn null. Cách thay thế, một giá trị "ma thuật" như -1 hay DateTime.MinValue, trông giống dữ liệu thật và sớm muộn cũng bị dùng như dữ liệu thật.
int? chỉ chuyển sang int khi ép kiểu tường minh, vì phép chuyển có thể thất bại. Điều đó dẫn tới ngoại lệ duy nhất mà kiểu giá trị nullable ném ra.
"Nullable object must have a value"
Đọc .Value khi không có giá trị, hoặc ép một int? đang null sang int, sẽ ném InvalidOperationException:
Đầu ra:
InvalidOperationException: Nullable object must have a value.
0
0
not stocked
stock is int count là một mẫu (pattern): nó kiểm tra có giá trị hay không và lấy giá trị ra biến count trong một bước, dễ đọc hơn HasValue cộng Value. Trang về pattern matching có thêm nhiều dạng khác.
Phép toán và so sánh với null
Các toán tử trên kiểu giá trị nullable được nâng (lifted): nếu một trong hai toán hạng là null thì kết quả là null. Các phép so sánh thứ tự (<, >, <=, >=) là ngoại lệ: chúng trả về false thay vì null. (Ngoại lệ còn lại là bool? với & và |, khi đó null & false là false.) Ví dụ:
Đầu ra:
True
[]
False
False
True
True
Cả a > 5 lẫn a <= 5 đều là false, nên !(a > 5) không giống a <= 5 khi a là null. Đoạn code như if (!(age >= 18)) Deny(); coi tuổi bị thiếu là chưa đủ tuổi, điều có thể đúng hoặc không đúng ý bạn. Hãy quyết định null nên mang nghĩa gì trước, thường bằng ??, rồi mới so sánh.
Toán tử null-coalescing ??
a ?? b cho kết quả a khi a khác null, ngược lại cho b. Toán tử này dùng được với cả kiểu giá trị nullable lẫn kiểu tham chiếu, và nối chuỗi từ trái sang phải:
Đầu ra:
dark
16px
20
ANONYMOUS
Vế phải chỉ được tính khi cần, nên cache ?? LoadFromDatabase() không chạm tới cơ sở dữ liệu khi cache đã có giá trị. ?? cũng nhận một biểu thức throw, tạo thành một câu lệnh bảo vệ gọn gàng (xem throw):
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
??= (C# 8)
x ??= y chỉ gán y cho x khi x là null. Đây là cách viết quen thuộc cho khởi tạo lười (lazy initialization):
private List<string> tags;
public void AddTag(string tag)
{
tags ??= new List<string>(); // C# 8 and later
tags.Add(tag);
}
Trước C# 8, hãy viết tags = tags ?? new List<string>(); hoặc if (tags == null) tags = new List<string>();.
Toán tử null-conditional ?.
a?.Member chỉ tính Member khi a khác null; ngược lại cả biểu thức là null. Toán tử này biến một chuỗi kiểm tra null thành một biểu thức duy nhất:
Đầu ra:
Braga
(no city)
(no customer)
0
True
handler: saved
Những chi tiết nên biết:
- Toán tử này ngắt mạch (short-circuit) phần còn lại của chuỗi: trong
nobody?.Address?.City, khinobodylà null thì mọi thứ bên phải đều không chạy. - Khi thành viên trả về một kiểu giá trị, kết quả trở thành nullable:
Orders?.Countlàint?, vì thế cần?? 0để trở lại thànhint. ?.không thể đứng ở vế trái của phép gán trước C# 14:customer?.Name = "x"không biên dịch được ở các phiên bản cũ hơn.handler?.Invoke(...)là cách chuẩn để kích hoạt một sự kiện hoặc gọi một callback không bắt buộc.
Hãy dùng ?. ở những chỗ null là trạng thái hợp lệ. Rải nó khắp nơi để tránh ngoại lệ sẽ che giấu bug đã sinh ra giá trị null bất ngờ, và chương trình tiếp tục chạy với dữ liệu thiếu thay vì báo lỗi ngay tại chỗ có vấn đề.
Kiểm tra null
if (customer == null) { ... } // classic
if (customer is null) { ... } // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... } // C# 9
if (customer is { } c) { ... } // C# 8 property pattern: not null, and named
is null luôn kiểm tra null thật sự, còn == gọi toán tử nạp chồng của kiểu nếu có, nên một bản nạp chồng viết tệ có thể khiến x == null trả lời sai. Với các đối số không được phép null, hãy báo lỗi sớm:
public void Ship(Order order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
// ...
}
Kiểu tham chiếu nullable (C# 8)
Thêm ? vào một kiểu giá trị sẽ thay đổi kiểu đó. Thêm ? vào một kiểu tham chiếu, từ C# 8, chỉ thay đổi những gì trình biên dịch kiểm tra. Khi tính năng được bật (<Nullable>enable</Nullable> trong tệp dự án, mặc định cho các dự án mới từ .NET 6), string nghĩa là "không bao giờ nên null" còn string? nghĩa là "có thể null", và trình biên dịch cảnh báo khi code không khớp với điều đó:
#nullable enable
public class User
{
public string Email { get; set; } // warning CS8618: Non-nullable property 'Email'
// must contain a non-null value when exiting constructor.
public string? Nickname { get; set; } // allowed to be null
public int NicknameLength() => Nickname.Length;
// warning CS8602: Dereference of a possibly null reference.
public int SafeLength() => Nickname?.Length ?? 0; // no warning
}
string? input = Console.ReadLine();
string name = input; // warning CS8600: Converting null literal or possible
// null value to non-nullable type.
string checkedName = input ?? "guest"; // no warning
int len = input!.Length; // ! tells the compiler "trust me": no warning
Trình biên dịch theo dõi các phép kiểm tra của bạn xuyên suốt phương thức: sau if (input != null), input được coi là khác null. Toán tử ! (null-forgiving) tắt một cảnh báo khi bạn biết rõ hơn phân tích của trình biên dịch; mỗi lần dùng là một lời khẳng định mà trình biên dịch không kiểm chứng được, nên hãy dùng thật ít.
Hai điều giúp gỡ phần lớn nhầm lẫn. Thứ nhất, đây chỉ là cảnh báo: chương trình vẫn chạy như cũ, và một string vẫn có thể chứa null lúc chạy nếu nó đến từ code không có chú thích nullable. Thứ hai, string? không phải Nullable<string>; nó không có HasValue.
Lỗi thường gặp
- Dùng
.Valuemà không kiểm tra. NémInvalidOperationExceptionkhi giá trị là null. Hãy dùng??,GetValueOrDefault()hoặcis int n. - Cho rằng
!(x > 5)nghĩa làx <= 5. Không đúng khixlà null: mọi phép so sánh thứ tự với null đều là false. - Bọc mọi giá trị null bằng
?.. Cách này chỉ dập tắt triệu chứng và đẩy chỗ lỗi xa khỏi nguyên nhân. - Coi cảnh báo nullable reference là nhiễu. Mỗi cảnh báo đánh dấu một dòng có thể ném
NullReferenceException. Hãy sửa luồng xử lý hoặc nói rõ ý định bằng?. - Dùng giá trị ma thuật thay cho null.
-1,0vàDateTime.MinValueđể chỉ "không rõ" trông giống dữ liệu thật. Hãy dùngint?vàDateTime?.
Câu hỏi thường gặp
int? trong C# nghĩa là gì?
int? là cách viết tắt của Nullable<int>: một int có thể mang giá trị null. Các kiểu giá trị như int, decimal, bool và DateTime bình thường luôn có giá trị; thêm ? cho chúng một trạng thái "không có giá trị", đúng thứ cần cho một trường form bị bỏ trống hay một cột NULL trong cơ sở dữ liệu. Kiểm tra bằng HasValue hoặc != null và đọc giá trị bằng Value hoặc GetValueOrDefault().
Toán tử ?? trong C# làm gì?
a ?? b là toán tử null-coalescing: kết quả là a nếu a khác null, ngược lại là b. string name = input ?? "guest"; cho giá trị dự phòng chỉ trong một biểu thức. Toán tử này nối chuỗi được (a ?? b ?? c lấy giá trị khác null đầu tiên) và b chỉ được tính khi cần. ??= (C# 8) chỉ gán vế phải khi biến đang là null.
Toán tử ?. trong C# làm gì?
a?.B là toán tử null-conditional: nếu a là null thì cả biểu thức là null và B không bao giờ được tính, nên không có NullReferenceException. Toán tử này nối chuỗi được (order?.Customer?.Address?.City) và dùng được cho lời gọi phương thức (logger?.Log(...)), sự kiện (Changed?.Invoke(...)) và indexer (items?[0]). Kết hợp với ?? để có giá trị mặc định: order?.Customer?.Name ?? "unknown".
Lỗi "Nullable object must have a value" do đâu?
Đọc .Value của một kiểu giá trị nullable đang là null, hoặc ép nó sang kiểu không nullable ((int)maybe), sẽ ném InvalidOperationException kèm thông báo đó. Hãy kiểm tra HasValue trước, hoặc dùng GetValueOrDefault(), ?? fallback hay một mẫu (pattern): if (maybe is int n) { ... }.
Làm sao kiểm tra null trong C#?
x == null và x is null đều dùng được; is null (C# 7) không bị ảnh hưởng bởi kiểu nạp chồng ==, nên nhiều codebase ưu tiên cách này. Phủ định là x != null, hoặc x is not null trong C# 9. Để từ chối đối số null, dùng ArgumentNullException.ThrowIfNull(x) (.NET 6) hoặc _ = x ?? throw new ArgumentNullException(nameof(x)); trong code cũ hơn.
Kiểu tham chiếu nullable trong C# là gì?
Một tính năng của trình biên dịch C# 8, bật bằng <Nullable>enable</Nullable> (mặc định trong các dự án .NET 6+ mới). Khi bật, string nghĩa là "không bao giờ null" còn string? nghĩa là "có thể null", và trình biên dịch cảnh báo khi bạn có thể truy cập vào null (CS8602) hoặc gán null cho biến không nullable (CS8600). Tính năng này chỉ thay đổi cảnh báo, không thay đổi hành vi lúc chạy.