四个"小"特性,加起来省掉 30% 的日常样板代码——而且每天都在用
not / and / or + 关系模式Relational Patterns)、Target-typed new 表达式目标类型 new、协变返回Covariant Returns。四个特性覆盖 4 种不同的日常痛点。
打开任何一个 C# 教程,第一个程序长这样:
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
12 行,其中只有 1 行在做实际的事。 其余 11 行是仪式Ceremony——编译器完全知道你要干什么,但你被迫写出来。
Python 程序员嘲笑 C# 时,第一刀就砍在这里。
C# 9 的回答:
// 这就是一个完整的、可编译运行的 C# 程序
Console.WriteLine("Hello World!");
一行。没有 using(自动隐式导入)、没有 namespace、没有 class、没有 Main。
这不是给初学者用的"简易模式"——这是合法的、完整的 C# 程序。生成的 IL 和一个完整 Program.cs 一模一样。这就是本课要讲的第一个特性:顶级语句Top-level Statements。
// 完整示例:有 using、有方法、有 await、有参数
using System;
using System.Net.Http;
var client = new HttpClient();
var response = await client.GetStringAsync("https://api.github.com/zen");
Console.WriteLine(response);
// args 依然可用——编译器偷偷传给你
Console.WriteLine($"参数数量: {args.Length}");
// 甚至可以返回值——变成 Main 的 exit code
return 0;
| 在 Main 里能做的事 | 顶级语句中? |
|---|---|
using 导入命名空间 | ✅ 文件开头 |
| 定义局部变量 | ✅ |
await 异步调用 | ✅ 编译器自动生成 async Main |
访问 args | ✅ 隐式可用 |
return 返回 exit code | ✅ return 0; |
| 定义类 / struct / 方法 | ✅ 放在语句之后 |
| 第二个文件也写顶级语句 | ❌ 一个项目只能有一个文件用顶级语句 |
你不是一直想问"编译器生成了什么"吗?来:
// 你写的(Program.cs 一行):
Console.WriteLine("Hello");
// ===== 编译器生成的(简化) =====
using System; // 隐式 using(SDK 自动加)
// 编译器合成 Program 类 + $Main 方法
internal class Program
{
private static void $Main(string[] args)
{
Console.WriteLine("Hello");
}
}
如果你用了 await:
// 你写的:
var data = await FetchAsync();
Console.WriteLine(data);
// 编译器生成:
internal class Program
{
private static async Task $Main(string[] args) // ← 自动 async Task
{
var data = await FetchAsync();
Console.WriteLine(data);
}
}
internal class Program,Main 方法改名叫 $Main。代码引用了 Program 类的地方(如 EF Core migration、ASP.NET Minimal API)照常工作——编译器的合成类和以前的 Program 合并。
| ✅ 适合 | ❌ 不适合 |
|---|---|
| 控制台小工具、脚本、Demo | 大型项目——只有一个文件能省略,反而不一致 |
| ASP.NET Core Minimal API 的 Program.cs | 需要显式 Main 签名(比如返回 Task<int>) |
| .NET 6+ 默认模板——它就是默认 | 团队有严格的代码风格规范要求显式定义 |
C# 8 给了你 switch 表达式、属性模式、元组模式。C# 9 给模式加了三个逻辑关键字和一类比较操作——让模式匹配从"够用"到"顺手"。
not — 反模式之前你写非空检查:
// C# 8 之前的几种写法,都不够好
if (obj != null) { } // 依赖运算符重载——可能被 class 重载 !=
if (!(obj is null)) { } // 正确但括号太多,读起来绕
if (obj is object) { } // 匹配所有非 null——但语义不直觉
C# 9:
if (obj is not null) { } // 干净、直观、安全——编译器保证不走 operator !=
// not 可以用在任何模式前面
if (x is not 0) { } // x 不是 0
if (s is not null and not "") { } // 非 null 且非空字符串
// switch 表达式中也很实用
return obj switch
{
null => "空",
not null => "非空", // 覆盖了 null 之外的所有情况
};
is not null 而不是 != null?is not null 是模式匹配——编译器直接生成 IL 层面的 null 检查,不经过任何 operator != 重载。即使有人把 != 重载成奇怪的逻辑,is not null 也不受影响。这是最安全、最"确定性"的 null 检查方式。从 C# 9 开始,这是推荐的写法。
and 和 or — 组合模式// and:同时满足多个条件(交集)
if (x is > 0 and < 100) { } // 0 < x < 100
return score switch
{
>= 0 and < 60 => "F",
>= 60 and < 80 => "C",
>= 80 and < 90 => "B",
>= 90 and <= 100 => "A",
};
// or:满足其中之一(并集)
if (status is "pending" or "processing") { } // 两个状态之一
return ch switch
{
'a' or 'e' or 'i' or 'o' or 'u' => "元音",
_ => "辅音",
};
// and + or 混用——注意结合性:and 优先级高于 or
x switch
{
not null and >= 0 or null => "null 或非负数", // 等价于: (not null and >= 0) or null
};
when。and 优先级高于 or(和 && vs || 一致)。但模式匹配中不能用 () 给子模式分组——这是 C# 目前的设计限制。以下三种解法覆盖所有复杂场景:
// 需求:用户角色是 admin 或 moderator,且状态是 active
// 期望:(admin or moderator) and active——但现在不能加括号
// ❌ 直接写 = 歧义,因为 and 优先级高于 or
// "admin" or "moderator" and "active" → 等价于 "admin" or ("moderator" and "active")
// 解法①:拆成两行——用显式 switch 分步判断,再组合
var isPrivileged = role switch { "admin" or "moderator" => true, _ => false };
var result = (isPrivileged, status) switch
{
(true, "active") => "允许",
_ => "拒绝",
};
// 解法②:用 when 把复杂逻辑移出模式——when 里随便用括号
return user switch
{
_ when (user.Role is "admin" or "moderator") && user.Status == "active"
=> "允许",
_ => "拒绝",
};
// 解法③:把 or 表达式独立成一条分支,歧义自然消失
// 场景:角色特权组 OR(普通用户 + 活跃状态)
return (role, status) switch
{
("admin" or "moderator", _) => "允许", // or 独占一行,无歧义
(_, "active") => "允许", // 另一条独立规则
_ => "拒绝",
};
原则:模式匹配不是银弹——当模式变复杂到你需要括号时,说明该拆了。when 里随便用括号,因为那是普通 C# 表达式。
关系模式就是把 >、>=、<、<= 直接写进模式里面——不再需要 when:
// C# 8:大小比较只能用 when
score switch
{
int n when n >= 90 => "A",
_ => "Other",
};
// C# 9:关系模式——直接写比较
score switch
{
>= 90 => "A",
>= 80 => "B",
>= 60 => "C",
< 60 => "F",
};
关系模式 + 属性模式 = 表达力爆炸:
// 订单折扣:VIP 且金额 >= 1000 → 20% off,非 VIP 且金额 >= 500 → 5% off
return order switch
{
{ IsVip: true, Total: >= 1000 } => 0.20m,
{ IsVip: true, Total: >= 0 } => 0.10m,
{ IsVip: false, Total: >= 500 } => 0.05m,
_ => 0m,
};
>=、<)用关系模式——更短、更直观。涉及方法调用(StartsWith)、跨变量比较(w == h)仍然用 when。关系模式不能调用方法,只能比较。
new Target-typed new从 C# 2.0 起,你可以写 var x = new Person()——类型从右边推断出左边。但有时你不想用 var:
// 你一直在忍受的重复:
List<string> names = new List<string>(); // List 写了两次
Dictionary<string, Person> map = new Dictionary<string, Person>(); // 又臭又长
// 用 var?可以——但类型驱动不了字段/属性的初始化
var names = new List<string>(); // 右边还是得写类型
C# 9 的答案:既然编译器知道左边是什么类型,右边就别再重复了。
// C# 9: 左边说了类型,右边 new() 就够了
List<string> names = new();
Dictionary<string, Person> map = new();
// 只要编译器能从上下文推出来类型,new() 就能用
ProcessData(new()); // 参数类型从方法签名推断
Person[] people = { new(), new(), new() }; // 数组元素类型从声明推断
// 字段初始化——之前必须写全,现在不需要
private List<Order> _orders = new(); // 字段类型已声明,右边简写
new() !== new 一切:前提是编译器能从单一、明确的目标上下文推断出类型。以下情况不能用:
// ❌ 不能:lambda 表达式参数类型不明
M(new()); // 如果 M 有多个重载——歧义
// ❌ 不能:var 声明——没有"目标类型"
var x = new(); // 编译器:你到底要什么类型?
// ❌ 不能:dynamic
dynamic d = new(); // dynamic 不提供编译期类型信息
| 场景 | C# 8 | C# 9 |
|---|---|---|
| 字段初始化 | private List<int> _nums = new List<int>(); | private List<int> _nums = new(); |
| 构造函数 DI | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | 同样可以用 throw new() |
| 方法参数 | Process(new List<string>()); | Process(new()); |
| 集合初始化器 | new List<string> { "a", "b" } | new() { "a", "b" }(需要上下文) |
简单原则:左边明确写了类型 → 右边 new()。左边用 var → 右边必须写全。你的 IDE(VS / Rider)会自动建议这个——跟着改就行。
这是本课最"低调但有用"的特性——你看一遍就会忘,但写工厂方法 / Builder / Fluent API 时会突然想起它。
// 基类:Clone 返回 Animal
public abstract class Animal
{
public abstract Animal Clone();
}
// 派生类:只能声明返回 Animal,实际 new 的是 Dog
public class Dog : Animal
{
public override Animal Clone() => new Dog();
}
// ===== 调用者视角 =====
Dog original = new Dog();
Dog dog = (Dog)original.Clone(); // 必须转型!即使你知道它是 Dog
C# 9:
public class Animal
{
public virtual Animal Clone() => new Animal();
}
public class Dog : Animal
{
// 重写方法可以返回更具体的类型!
public override Dog Clone() => new Dog();
}
// ===== 调用者视角 =====
Dog original = new Dog();
Dog dog = original.Clone(); // 不需要转型!编译器知道 Dog.Clone() 返回 Dog
| 规则 | 说明 |
|---|---|
| 返回类型必须是引用类型 | 只有 class / record 支持——值类型不行(struct 不支持协变) |
| 必须是更具体的类型 | Dog 是 Animal 的子类 ✅;反过来 Animal 是 Dog 的子类 ❌ |
| 只对引用类型协变 | 泛型参数不支持——List<Dog> 不能替代 List<Animal>(那是泛型协变的事) |
| 隐式转换关系 | 调用者拿基类引用时,返回值自动向上转型,完全透明 |
// 一个典型的 Fluent Builder 模式——C# 9 之后优雅多了
public abstract class QueryBuilder
{
public abstract QueryBuilder Where(string condition);
public abstract QueryBuilder OrderBy(string column);
public abstract string Build();
}
public class SqlBuilder : QueryBuilder
{
private readonly StringBuilder _sb = new();
public override SqlBuilder Where(string condition) // 返回 SqlBuilder!
{
_sb.Append(" WHERE ").Append(condition);
return this;
}
public override SqlBuilder OrderBy(string column)
{
_sb.Append(" ORDER BY ").Append(column);
return this;
}
public override string Build() => _sb.ToString();
}
// 使用时——链式调用,不需要 cast
var sql = new SqlBuilder()
.Where("Age > 18")
.OrderBy("Name DESC")
.Build();
override Dog Clone()——作为真正的方法体;② 一个隐式桥接方法 override Animal Clone()——它调用方法①,然后向上转型返回。调用者通过 Animal 引用调用时走桥接方法;通过 Dog 引用调用时直接走方法①。
来看看一个 ASP.NET Core Minimal API 的 Program.cs——这四个特性实际搭配起来的效果:
// 现代 .NET(5+)Program.cs —— 四个 C# 9 特性都在里面
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args); // ① 顶级语句
builder.Services.AddSingleton<Database>(new()); // ④ Target-typed new
var app = builder.Build();
app.MapGet("/score/{n}", (int n) => n switch // ② 模式匹配增强
{
>= 90 and <= 100 => "A",
>= 80 and < 90 => "B",
>= 60 and < 80 => "C",
>= 0 and < 60 => "F",
_ => "Invalid",
});
app.Run();
// ③ 协变返回——ASP.NET Core 内部大量使用(如 IApplicationBuilder → WebApplication)
public class MyBuilder : BaseBuilder
{
public override MyBuilder AddLogging() { ... } // 返回更具体类型
}
这就是 C# 9 小特性的真正价值:单个看都不大,放在一起用才看得出效果。日常写代码时,这四个特性几乎每个文件都会碰到。
is not null 和 != null 的关键区别是什么?score switch
{
>= 80 => "B", // ①
>= 90 => "A", // ②
_ => "C", // ③
};
score = 95 时,返回什么?new() 用法是合法的?Program 类(用于集成测试)。以下哪个做法是正确的?and 和 or 组合模式,哪个描述是正确的?Lesson 08 · C# 9 小特性集 · 下一课预告:C# 10——Record Structs、Global Usings、File-scoped Namespaces、Constant Interpolated Strings
有任何不清楚的地方?直接在对话中追问——Agent 就是你的私教。