0Pricing
C# Academy · 课时

启用并理解 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 启用并理解 NRT
  2. 注解:?、!、MaybeNull 与 NotNull
  3. 空值条件与空值合并运算符
  4. 将代码库迁移到 NRT
← 返回 C# Academy