Nullable 是值类型的可空封装,T? 是其语法糖;引用类型 T? 表示可为空引用类型,语义与机制均不同。

什么是 Nullable,它和 T? 是一回事吗
是的,T? 是 Nullable 的语法糖,仅适用于值类型(如 int、DateTime、bool)。引用类型(如 string、List)本身就能为 null,所以 string? 在 C# 8+ 中表示“可为空引用类型”,和值类型的 int? 语义不同,底层机制也完全不同。
关键区别在于:值类型加 ? 后,编译器生成的是 Nullable 结构体,它包含两个字段:value(真实值)和 hasValue(是否已赋值),不是简单包装指针。
-
int? x = null;→ 实际是new Nullable,() hasValue == false -
int? y = 5;→ 实际是new Nullable,(5) hasValue == true -
typeof(int?) == typeof(Nullable为) true
解包时用 .Value 还是 .GetValueOrDefault()
直接访问 .Value 会在 hasValue == false 时抛出 InvalidOperationException:“Nullable 对象必须具有一个值”。这在运行时才暴露,容易漏测。
.GetValueOrDefault() 更安全:未赋值时返回 T 的默认值(如 int?.GetValueOrDefault() 返回 0),不抛异常。但要注意——它掩盖了“本应有值却缺失”的业务逻辑问题。
- 用
.Value:适合你**确定有值**的场景(比如刚做过HasValue检查或来自可信输入) - 用
.GetValueOrDefault():适合提供兜底值即可的场景(如数据库字段可能为空,前端显示“0”比崩溃好) - 更推荐显式空合并:
int? x = ...; int actual = x ?? -1;,语义清晰且不可空
== null 和 HasValue == false 等价吗
等价,但编译后行为略有差异。C# 编译器对 == null 做了特殊处理,会直接检查 hasValue 字段,和手动写 !x.HasValue 生成的 IL 几乎一致。
不过要注意:重载了 == 运算符的自定义结构体,如果错误地把 Nullable 当普通类型处理,可能出错。标准值类型(int、Guid 等)完全安全。
-
int? x = null; if (x == null) { ... }✅ 安全、推荐 -
if (!x.HasValue) { ... }✅ 语义更直白,尤其在复杂条件中 -
if (x.Equals(null)) { ... }❌ 不推荐,调用虚方法,且null传入可能触发装箱
数据库读取时 DbDataReader.GetFieldValue() 和 GetInt32() 的坑
ADO.NET 的 GetFieldValue 支持泛型,但对可空类型要求严格:若数据库列允许 NULL,且你传入 int?,它能正确返回;但若误传 int,遇到 NULL 会直接抛 InvalidCastException。
而传统方法如 GetInt32()、GetString() 全部不支持 NULL —— 遇到 NULL 就炸,必须先用 IsDBNull() 判断。
var reader = cmd.ExecuteReader();
while (reader.Read())
{
// ✅ 安全:自动处理 NULL
int? age = reader.GetFieldValue(reader.GetOrdinal("age"));
// ❌ 危险:NULL 时抛异常
// int age2 = reader.GetInt32(reader.GetOrdinal("age"));
// ✅ 传统方式(啰嗦但明确)
var dbVal = reader["age"];
int? age3 = dbVal == DBNull.Value ? null : (int?)dbVal;}
最易被忽略的一点:Entity Framework Core 的 FromSqlRaw 或原生查询返回匿名类型时,若字段为 NULL,对应属性会是默认值(如 0),而不是 null —— 因为匿名类型属性推导基于非空类型。这时必须显式声明为可空,或改用元组/具体 DTO。










