1. 入门
想象一下,代码就像一场戏,变量就是演员。每次变量进入新场景(代码块),它可能是“活着的”(有值)或者“幽灵”(null)。但问题来了:观众——编译器——不是现场看戏,而是看剧本。他能看到演员啥时候被分配角色(赋值),啥时候消失(变成null)。但他没法看到现实(程序运行),只能根据剧本猜!
静态数据流分析,就是编译器在程序运行前检查代码,看看会不会有人演出“NullReferenceException崩溃”的戏码。
过程本质
从C# 8.0和Nullable Reference Types(NRT)模式开始,编译器会分析变量在某些执行分支里会不会变成null。如果有可能——它立马告诉你。它不会让你“用一个突然变成幽灵的演员的名字”。
2. 怎么看懂编译器警告
主要警告类型
NRT模式开着的时候,编译器会特别关注引用类型变量(string、User、数组、对象)。最常见的警告有这些:
- Possible null assignment —— 你试图把可能是null的值赋给不允许null的变量。
- Dereference of a possibly null reference —— 你访问对象成员,但编译器怀疑它可能是null。
- Possible null return —— 方法可能返回null,但签名要求必须有值。
- Nullable object must have a value —— 你在nullable类型上用.Value,但没检查。
- Unassigned non-nullable property —— 你没给不允许null的字段/属性赋值。
一般在IDE(比如Rider或Visual Studio)里,这些警告会用波浪线标出来,鼠标悬停会有解释。
典型警告示例
#nullable enable
string? possibleNull = GetString();
// 哎呀,编译器警告:"Dereference of a possibly null reference"
int length = possibleNull.Length; // 警告!
编译器不确定possibleNull是不是null,所以访问.Length可能会抛异常。
如果方法长这样:
string? GetString() { ... }
你写:
string nonNullable = GetString();
就会收到警告:可能把null不安全地传给non-nullable变量。
3. 编译器分析你的代码
例1:直接赋值
string? name = null;
Console.WriteLine(name.Length); // 警告:可能访问null
这里很明显:name是null,所以访问属性有风险。
例2:判断null
string? name = GetUserName();
if (name != null)
{
Console.WriteLine(name.Length); // 没问题!
}
else
{
Console.WriteLine("名字没填!");
}
编译器知道在if (name != null)块里变量是安全的。这就是flow analysis——根据条件分析值的流向。
4. 编译器没法“看进方法里”
它只看签名——如果写着方法返回string?,它就信可能是null,哪怕里面总是返回字符串。
例3:方法返回值
string? GetMaybeName(bool useName)
{
if (useName)
return "Code Jedi";
else
return null;
}
void PrintLength()
{
string? name = GetMaybeName(false);
Console.WriteLine(name.Length); // 警告!
}
编译器看到GetMaybeName可能返回null,所以让你小心点。
5. 常见场景和警告
把nullable赋给non-nullable变量
string? maybeUser = GetUser();
string alwaysUser = maybeUser; // warning: possible null assignment
要消除警告:
string alwaysUser = maybeUser ?? "游客";
这样总会有字符串——要么来自方法,要么是“游客”。
忘了检查.HasValue的nullable值类型
int? age = GetAge();
int realAge = age.Value; // warning: 可能访问null
要么这样:
if (age.HasValue)
Console.WriteLine(age.Value);
或者:
int realAge = age ?? -1;
自动属性没初始化
class User
{
public string Name { get; set; } // warning: 没初始化!
}
解决方法:
public string Name { get; set; } = "无名";
或者
public string? Name { get; set; }
6. 数据流分析的复杂情况
循环里的分析
string? text = null;
while (text == null)
{
text = Console.ReadLine();
}
Console.WriteLine(text.Length); // 没问题:编译器懂了!
多种赋值路径
string? name;
if (Random.Shared.Next() % 2 == 0)
name = "Alice";
else
name = null;
Console.WriteLine(name.Length); // warning: possible null dereference
编译器会分析所有分支。只要有一个是null,它就会警告。
7. 怎么“安抚”编译器(以及什么时候别这么做)
有时候你很确定值不会是null,但编译器不信:
string? value = GetSomething();
if (value == null)
throw new Exception("需要有值!");
Console.WriteLine(value.Length); // 警告没了
或者你用!操作符:
string? value = GetSomething();
Console.WriteLine(value!.Length); // 没警告,但有风险
这叫null-forgiving operator。它啥都不检查——只是告诉编译器:“别担心,哥稳得很”。但你要是搞错了,运行时就崩。
GO TO FULL VERSION