启用并理解 NRT
启用可空上下文,了解可空与不可空引用类型,并阅读编译器警告。
启用并理解 NRT 是 CoddyKit 上的免费 C# Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
价值十亿美元的错误
Tony Hoare(null 的发明者)称其为自己的“价值十亿美元的错误”。在 C# 中,任何引用类型默认都可能为空值,因此 NullReferenceException 成为了最常见的运行时崩溃原因。可空引用类型(NRT)在编译器层面解决了这一问题。
启用可空上下文
您可以在项目文件中全局启用 NRT,也可以通过指令按文件启用。启用后,编译器默认将引用类型视为不可为空。
<!-- In .csproj — enable for the whole project -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// Or per-file:
#nullable enable
// ... code with NRT warnings
#nullable disable
// ... code without NRT warnings不可空与可空引用类型
启用 NRT 后:string 表示永不为空,string? 表示可能为空。如果将空值赋给不可空类型,或未检查就解引用可空值,编译器会发出警告。
#nullable enable
string nonNull = "Hello"; // OK
string? maybeNull = null; // OK — explicitly nullable
nonNull = null; // WARNING: CS8600
Console.WriteLine(maybeNull.Length); // WARNING: CS8602 — may be null
// Fix:
if (maybeNull is not null)
Console.WriteLine(maybeNull.Length); // safe流分析
编译器会执行流分析——它会通过条件语句和分支跟踪空值状态;当能够证明某个值不为空时,就会抑制相关警告。
string? name = GetName();
// After null check, name is treated as non-null:
if (name is not null)
Console.WriteLine(name.Length); // no warning
// Same with early return:
if (name is null) return;
Console.WriteLine(name.Length); // no warning — null was returned
// Pattern matching:
if (name is string s)
Console.WriteLine(s.ToUpper()); // no warning可空性警告:CS8600–CS8629
需要理解的 NRT 主要警告包括:CS8600(将空值赋给不可空类型)、CS8602(可能为空值的解引用)、CS8603(为不可空类型返回空值)、CS8618(未初始化的不可空字段)。
#nullable enable
public class Order
{
public string CustomerName { get; set; } // CS8618: not initialized
public string? TrackingNumber { get; set; } // OK — nullable
public Order(string name)
{
CustomerName = name; // now initialized — CS8618 gone
}
}
string? s = GetValue();
Console.WriteLine(s.Length); // CS8602: s might be null初始化不可空字段
当不可空字段或属性未在构造函数中设置时,就会触发 CS8618。解决方案包括:在声明时初始化、通过构造函数要求传入,或对 DI/ORM 模式使用空值宽恕运算符。
// Option 1: Initialize in declaration
public string Name { get; set; } = "";
// Option 2: Require via constructor
public class Product
{
public string Name { get; }
public Product(string name) => Name = name;
}
// Option 3: null-forgiving for ORM entities
public string Name { get; set; } = null!;
// null! tells compiler "trust me, this will be set by EF Core"泛型类型中的可空性
可以将泛型类型参数约束为不可空类型。where T : notnull 约束可确保 T 永远不会为空。
// Without constraint: T could be nullable
public T? Find<T>(int id) { ... }
// With notnull: T must be a non-nullable type
public T FindRequired<T>(int id) where T : notnull
{
var result = InternalFind<T>(id);
return result ?? throw new KeyNotFoundException();
}
// T? is meaningful only when T is known to be non-nullable
public T? FindOrDefault<T>(int id) where T : class { ... }接口和重写中的可空性
实现接口或重写方法时,可空性注释必须匹配。编译器会检查实现的可空性是否至少与接口声明的一样严格。
public interface IRepository<T>
{
T? FindById(int id); // may return null
T GetOrThrow(int id); // never null
}
// Implementation:
public class ProductRepo : IRepository<Product>
{
public Product? FindById(int id) => _db.Find(id);
public Product GetOrThrow(int id) =>
_db.Find(id) ?? throw new KeyNotFoundException();
}可空值类型与可空引用类型
不要混淆 int?(Nullable<int>——在 C# 8 之前就存在的值类型包装器)和 string?(NRT 注释——仅在编译时生效,不产生运行时开销)。
// Nullable value type (runtime Nullable<int>)
int? age = null;
bool hasValue = age.HasValue;
int value = age.GetValueOrDefault();
// Nullable reference type (compile-time annotation only)
string? name = null;
// string? has NO runtime wrapper — it's just a compile-time hint
// null check is still needed at runtime
if (name is not null)
Console.WriteLine(name.ToUpper());使用 ! 抑制警告
空值宽恕运算符(!)可在您比编译器更确定时,抑制特定的可空性警告。请谨慎使用,并说明其安全原因。
// Use ! when you know the value cannot be null
var user = _db.Users.FirstOrDefault(u => u.Email == email);
var name = user!.Name; // user is guaranteed by business logic
// EF Core navigation properties set by the framework:
public class Order
{
public Customer Customer { get; set; } = null!; // set by EF Core
}实际应用:可空 API 响应
API 包装器中的字段可能存在,也可能不存在于 JSON 响应中;可空性注释可以清晰地表达这一意图。
#nullable enable
public class WeatherResponse
{
public string City { get; set; } = ""; // always present
public double Temperature { get; set; } // always present
public string? Description { get; set; } // optional field
public string? AlertMessage { get; set; } // only when there's an alert
}
// Consumer:
var weather = await api.GetWeatherAsync("London");
Console.WriteLine(weather.City); // safe
Console.WriteLine(weather.Description?.ToUpper() ?? "N/A"); // safe null-handling快速检查
与普通引用类型(string)相比,可空引用类型(string?)会产生多少运行时开销?
回顾:启用并理解 NRT
要点:
- 在 .csproj 中使用
<Nullable>enable</Nullable>全局启用 string= 永不为空;string?= 可能为空- 流分析会通过分支和返回值跟踪空值状态
- NRT 注释仅在编译时生效,不产生任何运行时开销
- CS8618:在构造函数中初始化不可空字段,或在 ORM 中使用
= null! - 谨慎使用 !,并说明空值宽恕操作安全的原因
常见问题解答
「启用并理解 NRT」课时是免费的吗?
是的 — 「启用并理解 NRT」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「启用并理解 NRT」这节课中我会学到什么?
启用可空上下文,了解可空与不可空引用类型,并阅读编译器警告。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「启用并理解 NRT」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。