在 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 |
四、常见问题与解决
-
错误:不可空类型接收
nullstring 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 响应)时至关重要。
