C# 正在经历一次深刻的内存安全变革。Microsoft 于 2026 年 5 月发布的 C# 16 内存安全模型,将 unsafe 关键字从单纯的语法标记转变为显式的调用方契约,这一变化标志着 .NET 生态向更严格的内存安全审计迈出了关键一步。
从语法标记到安全契约
C# 1.0 引入的 unsafe 关键字最初只是为了建立不安全上下文,让开发者能够在特定范围内使用指针特性。这种设计虽然灵活,但存在一个根本问题:它只标记了 "什么代码可以使用指针",却没有明确传达 "调用这段代码需要满足什么前提条件"。
C# 16 重新诠释了 unsafe 的语义,使其从 "标记一种语法" 转变为 "标记一种契约"—— 即编译器无法验证、必须由开发者阅读并遵守的内存安全约定。这种转变与 Rust 和 Swift 的安全模型趋同,让 C# 成为继 Rust 之后又一个在语言层面强制内存安全传播的主流语言。
四层安全机制
新模型建立了一套分层的安全机制,将不安全操作通过调用图逐层传递:
内层 unsafe { } 块:所有不安全操作(调用 unsafe 成员、解引用指针等)必须出现在内层 unsafe 块中。这是基础机制,让不安全操作在语法上被显式标记、限定范围并可审查。
传播机制:在方法签名上添加 unsafe 会将内层块的义务重新发布给调用方,除非在边界处解除。这就在调用图中划分出了安全方法、unsafe 方法以及两者之间的边界方法。
安全文档:每个 unsafe 成员都应携带 /// <safety> 文档块,这是被调用方与调用方之间的正式契约。虽然编译器不会强制要求,但分析器会标记缺失的安全文档。
边界解除:包含内层 unsafe 块但签名未标记 unsafe 的方法,构成了不安全代码与安全代码之间的边界。它通过运行时守卫、静态推理或上游 API 的文档化不变量来解除被调用方的义务。
与 Rust 安全模型的对比
C# 16 的安全模型与 Rust 高度相似,都采用显式的 unsafe 传播机制。关键区别在于:C# 16 中指针类型在签名中不再自动传播不安全特性,只有指针解引用本身才是不安全的;而 Swift 则采用隐式传播,任何 @unsafe 类型出现在签名中都会使声明隐式变为 @unsafe。
这种设计选择让 C# 和 Rust 都偏向简单明确的规则,降低了领域知识门槛。正如 Microsoft 团队所言,使用 grep 作为安全审计工具在 C# 16 和 Rust 中都是合理的,因为显式关键字提供了易于查询的锚点。
项目级配置与迁移策略
新模型通过两个独立的项目级开关控制:
-
新安全模型开关(最终名称将在 .NET 11 预览版中确定):开启后应用新的调用方不安全规则,关闭则保持 C# 1.0 行为。
-
<AllowUnsafeBlocks>:默认为false,控制项目中是否允许出现unsafe关键字。
推荐配置组合:
- 新开关开启 +
<AllowUnsafeBlocks>关闭:最安全配置,参与新模型且不允许任何不安全代码 - 新开关开启 +
<AllowUnsafeBlocks>开启:参与新模型且允许不安全代码
Microsoft 计划提供 dotnet format 修复工具,自动将未启用新属性的项目中的不安全调用点包装在 unsafe { } 块中,并将类型的 unsafe 修饰符迁移到其成员上。这可以完成机械性的重写,但安全义务推断和 <safety> 文档编写仍需开发者手动处理。
实际迁移示例
考虑 Marshal.ReadByte 方法的迁移。当前实现:
public static unsafe byte ReadByte(IntPtr ptr, int ofs)
{
byte* addr = (byte*)ptr + ofs;
return *addr;
}
在新模型下变为:
/// <safety>
/// ptr 与 ofs 的和必须指向调用方有权读取的字节。
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int ofs)
{
byte* addr = (byte*)ptr;
unsafe
{
// SAFETY: 依赖调用方义务。
return addr[ofs];
}
}
关键变化:指针解引用 addr[ofs] 被显式包裹在 unsafe 块中,签名上的 unsafe 成为调用方面向的契约,强制调用方在调用点也使用 unsafe 块。
边界防护与代码审查
不安全边界方法是安全审计的起点。良好的边界方法应该:
- 在方法体中进行充分的输入验证(如
ArgumentNullException.ThrowIfNull、ArgumentOutOfRangeException.ThrowIfNegative) - 使用
try/finally确保资源释放 - 通过
// SAFETY:注释说明解除义务的依据
以 String.CopyTo 为例,它在调用 Buffer.Memmove 之前执行了六项边界检查,每项检查都支撑着一个原始内存操作假设的不变量。
AI 时代的安全考量
新模型为 AI 代码生成提供了两个不可忽略的机制:调用图被划分为安全、unsafe 和边界方法;编译器会拒绝没有 enclosing unsafe 块的 unsafe 调用。分析器还会对缺失 <safety> 文档发出警告。这些约束显著缩小了 AI 能够生成且保持构建成功的代码范围。
研究表明,将类型系统约束嵌入 LLM 解码过程(而非依赖事后编译器反馈)可以将编译错误减少一半以上。C# 16 的更严格语法规则正是朝着这个方向迈进。
结论
C# 16 的内存安全改进不是对现有代码的破坏性重构,而是一套渐进式的增强机制。通过项目级 opt-in 开关,团队可以按自己的节奏迁移。核心收益在于:一旦启用新模型且保持 <AllowUnsafeBlocks> 为默认关闭状态,内存安全审计就从审查每个 diff 简化为检查一个项目属性 —— 编译器会拒绝任何不安全代码。
这种设计让 C# 在保持向后兼容的同时,为追求极致内存安全的团队提供了工业级的工具支持。随着 .NET 运行时库本身的迁移推进,生态系统的整体安全水位将得到系统性提升。
参考来源
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。