5个被严重低估的 C# 特性,让防御性代码彻底消失
现代C#通过类型系统革新大幅减少防御性代码:1.required成员强制初始化必要字段;2.init-only属性实现真正不可变;3.ConfigureAwaitOptions细化异步控制;4.CallerArgumentExpression自动生成错误上下文;5.流属性注解提升空安全分析。这些特性将运行时检查转为编译期保障,使代码更简洁可靠,让开发者专注于业务逻辑而非参数校验,显著提升系统稳定性
很多老派 .NET 开发者都有一个共同习惯:写一堆防御性代码。
方法开头先来三行 null 检查,构造函数里各种参数验证,属性全部 private set “以防万一”,单元测试也围绕“不变量有没有被破坏”反复验证。
但说实话,这些代码大多不是业务逻辑,而是对“可能出错”的恐惧。
从 C# 10 到 C# 13,语言本身已经进化到一个新的阶段。很多过去只能在运行时做的防御,现在可以在编译期直接解决。这不是语法糖,而是类型系统和流分析能力的提升。
下面这 5 个特性,如果用好,能明显减少守卫代码,让系统更清晰、更可靠。
1. required 成员:把不变量交给编译器
过去我们经常会写出这样的代码:
var user = new User { Name = "Ali" }; // Email 被遗漏,编译器却无动于衷
如果 Email 是业务必须字段,这种“半初始化对象”迟早会在生产环境炸锅。
C# 11 引入了 required 成员:
public sealed class User
{
public required string Name { get; init; }
public required string Email { get; init; }
}
只要少写一个属性,编译器立刻报错。
这背后的价值很大:让无效状态无法被表示。
你不再需要在每个方法里写:
if (string.IsNullOrWhiteSpace(user.Email)) ...
因为类型已经保证它存在。
官方说明见: ① https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-11#required-members
2. init-only 属性:真正的“构造后不可变”
很多人以为 private set 就是不可变:
public class Order
{
public DateTime CreatedAt { get; private set; }
public void Recalculate()
=> CreatedAt = DateTime.UtcNow; // 其实还能改
}
这其实是假不可变。类内部随时可以改。
init 才是真正的生命周期约束:
public sealed class Order
{
public required DateTime CreatedAt { get; init; }
}
对象初始化之后,再赋值就会直接编译错误:
order.CreatedAt = DateTime.UtcNow.AddDays(-1); // 编译失败
这带来的不是“语法优雅”,而是对象图稳定性。尤其在高并发系统中,不可变对象意味着线程安全更简单,行为更可预测。
官方文档: ② https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init
3. ConfigureAwaitOptions:异步语义变得可控
过去我们只有:
await task.ConfigureAwait(false);
很多人用它,但并不真正理解它。
.NET 8 引入了 ConfigureAwaitOptions,让意图更明确。例如:
await SomeWork().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
在管道或框架级代码中,这非常有用:
public async Task ExecuteAsync(Func<Task> handler)
{
try
{
await handler().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
catch (Exception ex)
{
_logger.LogError(ex, "Pipeline failure.");
}
}
你可以清楚表达“异常如何传播”“是否继续执行”等语义,而不是靠约定。
相关说明可参考 .NET 博客文章: ③ https://devblogs.microsoft.com/dotnet/
4. CallerArgumentExpression:让守卫代码自解释
传统参数检查通常这样写:
if (email == null)
throw new ArgumentNullException(nameof(email));
错误信息基本靠 nameof 拼接。
C# 11 提供了 CallerArgumentExpression,可以自动捕获调用表达式:
public static void NotEmpty(
string value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException($"Invalid value: {expression}");
}
调用:
NotEmpty(user.Email);
异常信息会自动变成:
Invalid value: user.Email
这在验证库中尤其好用:
Ensure.NotNull(order.Items);
Ensure.NotEmpty(order.Customer.Email);
守卫逻辑变得更干净,错误信息也更有上下文。
官方文档: ④ https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute
5. 可空性流属性:告诉编译器你的真实意图
开启可空引用类型之后,我们经常遇到这种情况:
if (TryGetUser(id, out var user))
{
user.DoSomething(); // 编译器仍然警告
}
虽然逻辑上是安全的,但编译器不知道。
这时候可以用 [NotNullWhen(true)]:
public static bool TryGetUser(
int id,
[NotNullWhen(true)] out User? user)
{
// ...
}
现在,只要返回 true,编译器就相信 user 不为 null。
这对于公共库尤其重要,可以大幅减少 API 使用者的冗余 null 检查。
官方说明: ⑤ https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis
防御性代码 vs 声明式意图
|
传统方式 |
现代方式 |
|---|---|
|
运行时校验 |
编译期保障 |
|
手动守卫子句 |
流感知属性 |
|
可变对象 |
生命周期约束对象 |
|
异步经验主义 |
显式配置 |
|
字符串错误消息 |
表达式上下文 |
防御性代码从来不是目标,目标是正确性。
现代 C# 做的事情很简单:把“你希望成立的条件”写进类型系统,而不是写进 if 语句。
综合示例:五个特性协同工作
public sealedclassPayment
{
public required Guid Id { get; init; }
public required decimal Amount { get; init; }
public required string Currency { get; init; }
public static bool TryCreate(
decimal amount,
string currency,
[NotNullWhen(true)] out Payment? payment)
{
if (amount <= 0 || string.IsNullOrWhiteSpace(currency))
{
payment = null;
returnfalse;
}
payment = new Payment
{
Id = Guid.NewGuid(),
Amount = amount,
Currency = currency
};
returntrue;
}
}
你会发现,这里没有:
没有后续 null 检查。 没有对象构造后的二次校验。 没有防止属性被篡改的守卫逻辑。
因为类型已经表达了不变量。
架构层面的影响
当团队一致使用这些特性时,你会看到一些很明显的变化:
DTO 初始化错误明显减少。 空引用异常几乎消失。 守卫库代码越来越少。 单元测试更专注业务行为,而不是参数校验。
这不是“代码看起来更现代”,而是结构上的简化。系统规模越大,这种简化的复利效应越明显。
结语
现代 C# 的核心思想是:
不要假设开发者能记住所有不变量。 让语言帮你记住。
当类型系统足够精确,运行时就不需要那么多防御。
代码不只是能编译,而是能长期演进、能在规模下稳定运行。这才是语言进化真正的意义。
参考资料
① Microsoft. C# 11 required membershttps://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-11#required-members
② Microsoft. Init-only settershttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init
③ Microsoft. .NET Bloghttps://devblogs.microsoft.com/dotnet/
④ Microsoft. CallerArgumentExpressionAttributehttps://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute
⑤ Microsoft. Nullable flow attributeshttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis
⑥ Microsoft. .NET Bloghttps://devblogs.microsoft.com/dotnet/
更多推荐


所有评论(0)