C# 可空类型(Nullable)
在 C# 中,int、float、bool、DateTime 等都是值类型,它们默认必须有一个值,不能为 null:
int a = null; // 编译错误:无法将 null 转换为 int
可空类型(Nullable Types) 解决了这个问题——它让值类型额外拥有一个"没有值"的状态(即 null),在处理数据库字段、API 返回值等可能缺失数据的场景中非常实用。
声明方式很简单,在类型后面加一个 ?:
int? a = null; // 合法 int? b = 42; // 合法
这里的 int? 是 Nullable<int> 的语法糖(简写形式),两者完全等价:
Nullable<int> a = null; // 完整写法 int? a = null; // 简写,效果相同
下图展示了可空类型在内存中的工作方式——它用一个额外的布尔标记来记录"是否有值":
单问号 ? 与双问号 ?? 的区别
C# 中与可空类型相关的两个运算符经常一起使用,但含义完全不同:
| 运算符 | 名称 | 用途 | 示例 |
|---|---|---|---|
? |
可空类型修饰符 | 让值类型可以为 null |
int? i = 3; 等价于 Nullable<int> i = new Nullable<int>(3); |
?? |
空合并运算符(Null-Coalescing Operator) | 当变量为 null 时提供默认值 |
int result = i ?? 0; |
一个简单的对比:
int i; // 普通值类型,默认值为 0,永远不能是 null int? ii; // 可空类型,默认值为 null
可空类型的声明与赋值
声明语法
<data_type>? <variable_name> = null;
例如:
int? age = null; double? temperature = 36.6; bool? isActive = new bool?(); // 显式构造,默认为 null DateTime? birthday = null;
Nullable<T> 可以表示其基础值类型的正常范围,再加上一个 null 值。例如 Nullable<int> 可以是 -2,147,483,648 到 2,147,483,647 之间的任意整数,或者 null。
完整示例
实例
using System;
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
// 声明不同类型的可空变量
int? num1 = null;
int? num2 = 45;
double? num3 = new double?(); // null
double? num4 = 3.14157;
bool? boolVal = new bool?(); // null
// 显示值(null 会显示为空字符串)
Console.WriteLine($"num1 = {num1 ?? 0}"); // 0
Console.WriteLine($"num2 = {num2 ?? 0}"); // 45
Console.WriteLine($"num3 = {num3 ?? 0.0}"); // 0
Console.WriteLine($"num4 = {num4 ?? 0.0}"); // 3.14157
Console.WriteLine($"boolVal = {boolVal}"); // (空)
}
}
}
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
// 声明不同类型的可空变量
int? num1 = null;
int? num2 = 45;
double? num3 = new double?(); // null
double? num4 = 3.14157;
bool? boolVal = new bool?(); // null
// 显示值(null 会显示为空字符串)
Console.WriteLine($"num1 = {num1 ?? 0}"); // 0
Console.WriteLine($"num2 = {num2 ?? 0}"); // 45
Console.WriteLine($"num3 = {num3 ?? 0.0}"); // 0
Console.WriteLine($"num4 = {num4 ?? 0.0}"); // 3.14157
Console.WriteLine($"boolVal = {boolVal}"); // (空)
}
}
}
输出结果:
num1 = 0 num2 = 45 num3 = 0 num4 = 3.14157 boolVal =
Null 合并运算符(??)
?? 运算符用于为可空类型或引用类型提供一个兜底默认值。当左侧不为 null 时返回左侧值,否则返回右侧值:
<表达式1> ?? <表达式2>
- 如果
<表达式1>不为null,返回<表达式1>; - 否则返回
<表达式2>。
从 C# 8.0 起,还可以使用 ??=(空合并赋值运算符),仅当变量为 null 时才赋值:
int? x = null; x ??= 10; // x 为 null,赋值为 10 x ??= 20; // x 已有值 10,不再赋值
实例
using System;
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
double? num1 = null;
double? num2 = 3.14157;
// ?? 提供默认值
double result1 = num1 ?? 5.34; // num1 为 null → 返回 5.34
double result2 = num2 ?? 5.34; // num2 有值 → 返回 3.14157
Console.WriteLine($"num1 ?? 5.34 = {result1}"); // 5.34
Console.WriteLine($"num2 ?? 5.34 = {result2}"); // 3.14157
// 链式使用:提供多个备选值
int? a = null;
int? b = null;
int? c = 42;
int value = a ?? b ?? c ?? 0; // 依次尝试,最终得到 42
Console.WriteLine($"a ?? b ?? c ?? 0 = {value}"); // 42
}
}
}
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
double? num1 = null;
double? num2 = 3.14157;
// ?? 提供默认值
double result1 = num1 ?? 5.34; // num1 为 null → 返回 5.34
double result2 = num2 ?? 5.34; // num2 有值 → 返回 3.14157
Console.WriteLine($"num1 ?? 5.34 = {result1}"); // 5.34
Console.WriteLine($"num2 ?? 5.34 = {result2}"); // 3.14157
// 链式使用:提供多个备选值
int? a = null;
int? b = null;
int? c = 42;
int value = a ?? b ?? c ?? 0; // 依次尝试,最终得到 42
Console.WriteLine($"a ?? b ?? c ?? 0 = {value}"); // 42
}
}
}
输出结果:
num1 ?? 5.34 = 5.34 num2 ?? 5.34 = 3.14157 a ?? b ?? c ?? 0 = 42
可空类型的常用属性和方法
| 成员 | 说明 | 示例 |
|---|---|---|
.HasValue |
判断变量是否有值(返回 bool) |
if (num.HasValue) { ... } |
.Value |
获取实际值(若为 null 会抛 InvalidOperationException) |
int x = num.Value; |
.GetValueOrDefault() |
安全获取值,若为 null 返回类型的默认值(如 0) |
num.GetValueOrDefault() |
.GetValueOrDefault(T) |
安全获取值,若为 null 返回指定的默认值 |
num.GetValueOrDefault(100) |
?? |
空合并运算符(语法糖,等价于 GetValueOrDefault) |
int result = num ?? 100; |
实例
using System;
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
int? num = null;
// HasValue + Value(传统写法)
if (num.HasValue)
Console.WriteLine($"值为: {num.Value}");
else
Console.WriteLine("num 没有值");
// 安全获取默认值
Console.WriteLine(num.GetValueOrDefault()); // 0
Console.WriteLine(num.GetValueOrDefault(99)); // 99
Console.WriteLine(num ?? 99); // 99(等价写法)
// ⚠ 注意:直接访问 .Value 会抛异常
// int x = num.Value; // InvalidOperationException!
}
}
}
namespace NullableDemo
{
class Program
{
static void Main(string[] args)
{
int? num = null;
// HasValue + Value(传统写法)
if (num.HasValue)
Console.WriteLine($"值为: {num.Value}");
else
Console.WriteLine("num 没有值");
// 安全获取默认值
Console.WriteLine(num.GetValueOrDefault()); // 0
Console.WriteLine(num.GetValueOrDefault(99)); // 99
Console.WriteLine(num ?? 99); // 99(等价写法)
// ⚠ 注意:直接访问 .Value 会抛异常
// int x = num.Value; // InvalidOperationException!
}
}
}
实际应用场景
可空类型在实际开发中非常常见,尤其是处理可能缺失的数据时:
数据库字段映射
数据库中的字段允许为 NULL,用可空类型可以精确对应:
| 用户ID | 年龄 | 是否激活 |
|---|---|---|
| 1 | 28 | true |
| 2 | null | false |
int? age = GetUserAgeFromDB(userId: 2);
// 安全处理 null
string display = age.HasValue
? $"用户年龄:{age.Value}"
: "年龄未知";
// 或者更简洁的写法
string display2 = $"用户年龄:{age ?? 0}";
可选参数与配置
// 配置项可能未设置
int? timeout = GetConfigValue("timeout");
int actualTimeout = timeout ?? 30; // 未设置时使用默认值 30 秒
链式空值检查(C# 6.0+)
// 使用 ?. 运算符安全访问可能为 null 的对象 string? name = user?.Address?.City?.Name; // 如果 user、Address、City 中任何一个为 null,结果就是 null,不会抛异常
小结
| 功能 | 示例 | 说明 |
|---|---|---|
| 定义可空类型 | int? x = null; |
等价于 Nullable<int> |
| 判断是否有值 | x.HasValue |
返回 true 或 false |
| 获取值(不安全) | x.Value |
若为 null 会抛 InvalidOperationException |
| 获取默认值 | x ?? 0 |
若为 null 返回右侧值 |
| 获取指定默认值 | x.GetValueOrDefault(10) |
若为 null 返回 10 |
| 空合并赋值 | x ??= 10 |
仅当 x 为 null 时赋值(C# 8.0+) |
| 安全导航访问 | user?.Name |
若 user 为 null 则返回 null(C# 6.0+) |
C# 8.0 的"可空引用类型"
从 C# 8.0 开始,引入了 可空引用类型(Nullable Reference Types),它与本文介绍的可空值类型是两套不同的机制:
| 对比项 | 可空值类型 | 可空引用类型 |
|---|---|---|
| 示例 | int? |
string? |
| 作用对象 | 值类型(int、bool、struct 等) |
引用类型(string、class、数组等) |
| 实现方式 | 运行时通过 Nullable<T> 结构体实现 |
编译器静态分析,不改变运行时行为 |
| 默认状态 | 始终存在(从 C# 1.0 起) | 需在项目中启用 <Nullable>enable</Nullable> |
| null 检查 | 用 .HasValue 判断 |
编译器发出警告(非错误) |
可空引用类型是编译期的安全检查工具——它不会改变程序的运行时行为,但会在代码可能产生
NullReferenceException的地方发出编译器警告,帮助你在开发阶段就发现潜在问题。
