1值类型不能为空
有时候结果根本就没有,比如查一个人的年龄,数据库里偏偏没填。可 int、float、bool、DateTime 这些值类型必须有一个值,不能空着;硬给它赋值为空值 null,编译直接报错(本节例子都是顶级语句写法:不写类和 Main,代码从第一行开始执行):
| 1 | int a = null; | // 给普通 int 赋值为空值 |
Program.cs(1,9): error CS0037: 无法将 null 转换为“int”,因为后者是不可为 null 的值类型
2可空类型 int?
在类型后面加一个问号,int? 就是一个可以为空的 int。数据库字段、API(别的程序提供给你调用的接口)返回值这些可能缺数据的地方特别常用。int? 是简写,完整写法是 Nullable<int>:
| 1 | Nullable<int> a = null; // 完整写法 | // a 是空值 |
| 2 | int? b = null; // 简写,效果相同 | // b 也是空值 |
| 3 | Console.WriteLine(typeof(int?) == typeof(Nullable<int>)); | // typeof 取出类型来比较:两种写法是同一个类型 |
| 4 | Console.WriteLine(a == b); | // 两个空值相等 |
True True
3HasValue 和 Value
可空类型里有两样东西:Value 是值,HasValue 表示有没有值。
| 3 | int? x = 42; | // x 有值 42 |
| 4 | int? y = null; | // y 是空值 |
| 5 | Console.WriteLine($"x.HasValue = {x.HasValue}, x.Value = {x.Value}"); | // 有值:HasValue 是 True,Value 是 42 |
| 6 | Console.WriteLine($"y.HasValue = {y.HasValue}"); | // 没有值:HasValue 是 False |
| 7 | Console.WriteLine($"int 占 {Unsafe.SizeOf<int>()} 字节"); | // Unsafe.SizeOf 量出一个类型占几个字节:int 占 4 字节 |
| 8 | Console.WriteLine($"int? 占 {Unsafe.SizeOf<int?>()} 字节"); | // int? 占 8 字节 |
int? 比 int 多占地方:实测 int 占 4 字节,int? 占 8 字节,多出来的地方用来记“有没有值”(HasValue),外加内存对齐(让数据按 4 字节排整齐)补的空位。真实内存里 HasValue 就是一个布尔字段,记着真还是假。
4? 和 ??
两个长得很像的符号:一个问号 ? 是可空类型修饰符,让值类型可以为空,int? i = 3 等价于新建一个装着 3 的 Nullable<int>;两个问号 ?? 叫空合并运算符,变量为空时给一个默认值,比如 i ?? 0:i 为空,结果就是 0。
5默认值:局部变量要先赋值
int i; 和 int? ii; 只声明不赋值,直接打印,结果是编译错误:
| 1 | int i; // 普通值类型,默认值为 0,永远不能是 null | // 只声明,没赋值 |
| 2 | int? ii; // 可空类型,默认值为 null | // 只声明,没赋值 |
| 3 | Console.WriteLine(i); | // 使用没赋值的局部变量 |
| 4 | Console.WriteLine(ii); | // 同样没赋值 |
Program.cs(3,19): error CS0165: 使用了未赋值的局部变量“i” Program.cs(4,19): error CS0165: 使用了未赋值的局部变量“ii”
方法里声明的局部变量,不赋值就不能用。默认的 0 和空值只用在字段、数组元素和 default 这些地方:
| 1 | Console.WriteLine($"default(int) = {default(int)}"); | // default(类型) 取这个类型的默认值:int 是 0 |
| 2 | Console.WriteLine($"default(int?) == null:{default(int?) == null}"); | // int? 的默认值是空值 |
| 3 | int i = 0; | // 局部变量先赋值 0 |
| 4 | int? ii = null; | // 局部变量先赋值为空值 |
| 5 | Console.WriteLine($"i = {i}, ii.HasValue = {ii.HasValue}"); | // 赋过值就能用了:i = 0, ii.HasValue = False |
6声明可空变量
格式是:数据类型加问号,再写变量名。运行后只有体温 36.6 有值,另外三个的 HasValue 都是 False。
| 1 | int? age = null; | // 可空 int:年龄没填 |
| 2 | double? temperature = 36.6; | // 可空 double:体温 36.6 |
| 3 | bool? isActive = new bool?(); // 显式构造,默认为 null | // new bool?() 得到的也是空值 |
| 4 | DateTime? birthday = null; | // 可空 DateTime:生日没填 |
int? 能装下 int 的全部范围(实测从 -2147483648 到 2147483647),再加上一个空值。
7完整示例:打印可空变量
五个可空变量:num1、num3、boolVal 为空,num2 是 45,num4 是 3.14157;打印时前四个用 ?? 给了备用值。
| 17 | Console.WriteLine($"num1 = {num1 ?? 0}"); // 0 | // num1 为空,打印备用值 0 |
| 18 | Console.WriteLine($"num2 = {num2 ?? 0}"); // 45 | // num2 是 45,打印原值 |
| 19 | Console.WriteLine($"num3 = {num3 ?? 0.0}"); // 0 | // num3 为空,备用值 0.0 打印成 0 |
| 20 | Console.WriteLine($"num4 = {num4 ?? 0.0}"); // 3.14157 | // num4 是 3.14157,打印原值 |
| 21 | Console.WriteLine($"boolVal = {boolVal}"); // (空) | // boolVal 为空又没给备用值,等号后面什么也没有 |
8空合并运算符 ??
规则:左边不为空就取左边,为空就取右边。?? 还能连着写,依次试,谁先有值就取谁。
| 13 | double result1 = num1 ?? 5.34; // num1 为 null → 返回 5.34 | // num1 是空值,取右边 5.34 |
| 14 | double result2 = num2 ?? 5.34; // num2 有值 → 返回 3.14157 | // num2 有值,取它自己 |
| 23 | int value = a ?? b ?? c ?? 0; // 依次尝试,最终得到 42 | // a、b 都为空,c 是 42,取 42 |
从 C# 8.0 起还有 ??=:只在变量为空时才赋值。
| 1 | int? x = null; | // x 是空值 |
| 2 | x ??= 10; // x 为 null,赋值为 10 | // 第一次:x 为空,赋值为 10 |
| 3 | Console.WriteLine(x); | // 打印 10 |
| 4 | x ??= 20; // x 已有值 10,不再赋值 | // 第二次:x 已经有值,不改 |
| 5 | Console.WriteLine(x); | // 还是 10 |
9常用成员
HasValue:有没有值,返回 True 或 False。Value:取出真正的值,为空时会抛出异常。GetValueOrDefault():安全取值,为空时返回类型的默认值(比如 0);括号里写一个数,为空时就返回这个数。
示例三里 num 为空,先用 HasValue 判断,打印“num 没有值”,再安全取值:
| 18 | Console.WriteLine(num.GetValueOrDefault()); // 0 | // num 为空,返回 int 的默认值 0 |
| 19 | Console.WriteLine(num.GetValueOrDefault(99)); // 99 | // 为空,返回括号里的 99 |
| 20 | Console.WriteLine(num ?? 99); // 99(等价写法) | // ?? 也得到 99 |
直接取空值的 Value,程序当场崩溃,抛出 InvalidOperationException(可空对象必须有值):
| 1 | int? num = null; | // num 是空值 |
| 2 | int x = num.Value; | // 对空值取 Value:运行时抛异常 |
运行结果第一行是 Unhandled exception. System.。编辑器早有提醒,这一行有绿色波浪线:warning CS8629: 可为 null 的值类型可为 null。
?? 和 GetValueOrDefault 结果一样,但不完全等价:GetValueOrDefault 的备用值不管用不用得上都会先算一遍,?? 只在左边为空时才去算右边:
| 2 | int Backup() | // Backup 方法:算备用值 |
| 3 | { | // Backup 开始 |
| 4 | Console.WriteLine(" (算了一次备用值)"); | // 每被调用一次就打印一行 |
| 5 | return 100; | // 交回备用值 100 |
| 6 | } | // Backup 结束 |
| 7 | Console.WriteLine("GetValueOrDefault(Backup()):"); | // 打印标题 |
| 8 | Console.WriteLine(num.GetValueOrDefault(Backup())); | // num 是 5,用不上备用值,但 Backup() 还是先执行了 |
| 9 | Console.WriteLine("num ?? Backup():"); | // 打印标题 |
| 10 | Console.WriteLine(num ?? Backup()); | // num 不为空,Backup() 根本不执行 |
GetValueOrDefault(Backup()): (算了一次备用值) 5 num ?? Backup(): 5
10实际场景
数据库字段
2 号用户的年龄是空值,用 int? 接住,再用 HasValue 判断:
| 1 | int? age = GetUserAgeFromDB(userId: 2); | // 2 号用户的年龄没填,拿到空值 |
| 2 | | |
| 3 | // 安全处理 null | |
| 4 | string display = age.HasValue | // 有值吗? |
| 5 | ? $"用户年龄:{age.Value}" | // 有值:显示年龄 |
| 6 | : "年龄未知"; | // 没值:显示“年龄未知” |
| 7 | | |
| 8 | // 或者更简洁的写法 | |
| 9 | string display2 = $"用户年龄:{age ?? 0}"; | // 为空时用 0 代替 |
年龄未知 用户年龄:0
?? 0 的写法运行出来是“用户年龄:0”,像是刚出生。备用值要选有意义的,没填的年龄还是显示“年龄未知”更合适。
配置项
| 2 | int? timeout = GetConfigValue("timeout"); | // 配置里没写 timeout,拿到空值 |
| 3 | int actualTimeout = timeout ?? 30; // 未设置时使用默认值 30 秒 | // 为空,取默认的 30 |
链式检查 ?.
User、Address、City 是程序末尾定义的三个小类,一个套一个:
| 11 | class User { public Address? Address; } | // 用户里有一个可空的地址 |
| 12 | class Address { public City? City; } | // 地址里有一个可空的城市 |
| 13 | class City { public string? Name; } | // 城市里有一个可空的名字 |
一路往下取:用户、地址、城市、名字。只要哪一环为空,结果就是空值,不会抛异常:
| 1 | User? user = null; | // 用户是空值 |
| 2 | // 使用 ?. 运算符安全访问可能为 null 的对象 | |
| 3 | string? name = user?.Address?.City?.Name; | // user 为空,后面不再往下取,name 是空值 |
| 4 | // 如果 user、Address、City 中任何一个为 null,结果就是 null,不会抛异常 | |
| 5 | Console.WriteLine(name ?? "(null)"); | // 为空,打印“(null)” |
给每一环都填上值以后再取一遍,就取到了“青岛”。
11小结
- 定义可空类型,就在类型后面加问号。
- HasValue 判断有没有值,Value 直接取值但不安全。
??和 GetValueOrDefault 给备用值;??=只在为空时赋值;?.安全地一路往下取。
12可空引用类型
从 C# 8.0 开始还有一套可空引用类型,和今天讲的可空值类型是两套机制:可空值类型管 int、bool 这些值类型,运行时真有一个 Nullable 结构(struct,一种值类型,后面会讲)来装;可空引用类型管 string、类、数组,只是编译器在检查,不改变程序运行。
| 1 | string s = null; | // 编辑器给出绿色波浪线:警告 CS8600 |
| 2 | string? t = null; | // 加了问号,就没有警告 |
| 3 | Console.WriteLine(t == null); | // 打印 True |
| 4 | Console.WriteLine(s.Length); | // 警告只是提醒,程序照样运行,到这一行抛出空引用异常 |
运行先打印 True,再抛出 System.。
可空值类型是 C# 2.0 加入的。可空引用类型的检查,现在新建的项目默认就已经打开,不用自己启用。
下一节:想存全班五十个人的成绩,难道要声明五十个变量吗?
