在 C# 中,类型定义中的**问号(?)**主要用于控制类型的可空性,但具体行为因类型(值类型或引用类型)和 C# 版本而异。以下是清晰分类的说明:
一、可空值类型(T?,适用于所有 C# 版本)
用途 :允许值类型(如 int、DateTime 等)存储 null 值。
语法 :在值类型后加 ?,底层由 System.Nullable<T> 结构实现。
示例:
int? age = null; // 声明可空整型 DateTime? date = null; // 声明可空日期
核心操作:
-
判空 :通过
HasValue属性检查是否有值。if (age.HasValue) Console.WriteLine(age.Value); -
安全取值 :使用
??提供默认值,避免InvalidOperationException。int safeAge = age ?? 0; // 若 age 为 null,返回 0 -
强制转换 :直接将
int?赋值给int会报错,需显式转换。int value = (int)age; // 若 age 为 null,抛出异常
适用场景:
- 数据库字段可能为
null(如int?对应 SQL 中的NULL整数字段)。 - 需要区分"未赋值"和"有效值"(例如
0和null语义不同)。
二、可空引用类型(T?,C# 8.0+)
用途 :在严格模式下显式标记引用类型可为 null,避免空引用异常。
语法 :在引用类型后加 ?(需启用 #nullable enable)。
示例:
#nullable enable string? name = null; // 显式声明可为 null 的字符串 string title = null; // 严格模式下会警告:需改为 string?
核心规则:
-
严格模式 :启用后,引用类型默认不可为
null,需显式用?标记。#nullable enable public string? GetComment() { ... } // 可能返回 null -
安全访问 :使用
?.和??避免运行时异常。int length = name?.Length ?? 0; // 安全访问属性
适用场景:
-
API 设计:明确参数或返回值是否可为
null。public void SaveData(string id, string? optionalNote = null) { ... } -
反序列化 JSON 数据:处理可能缺失的字段。
public class User { public string Name { get; set; } // 必须存在 public string? Email { get; set; } // 允许为 null }
三、关键区别与注意事项
| 特性 | 可空值类型(int?) |
可空引用类型(string?) |
|---|---|---|
| 适用类型 | 值类型(struct) |
引用类型(class) |
| 底层实现 | System.Nullable<T> 结构 |
编译时静态分析,无运行时类型变化 |
| 默认可空性 | 必须用 ? 声明才可为 null |
严格模式下默认不可为 null |
| 运行时表现 | 实际存储为 T 或 null |
编译警告,但运行时仍可能为 null |
四、常见问题与解决
-
**错误:不可空类型接收
null**string name = null; // 严格模式下警告修复:
string? name = null; // 显式标记可空 -
错误:未处理可空值类型
int? age = null; int value = age; // 编译错误修复:
int value = age ?? 0; // 提供默认值 -
安全调用链
var length = user.Address?.City?.Length; // 避免多层 null 检查
五、最佳实践
- 启用严格模式 :在
.csproj中配置<Nullable>enable</Nullable>,提升代码安全性。 - 明确可空性 :公共 API 的参数和返回值显式标记
?。 - 防御性编程 :对可空类型进行判空(
if (x != null))或使用??、?.运算符。
通过合理使用 ?,可以显著减少空引用异常,提升代码健壮性,尤其在处理外部数据(如数据库、API 响应)时至关重要。