Lesson 08: C# 9 小特性集

四个"小"特性,加起来省掉 30% 的日常样板代码——而且每天都在用

前置 / Prerequisite:已完成 Lesson 07(C# 9 Records),熟悉 C# 8 模式匹配基础(Lesson 02)。
本课目标:掌握 Top-level Statements顶级语句、Pattern Matching 增强(not / and / or + 关系模式Relational Patterns)、Target-typed new 表达式目标类型 new、协变返回Covariant Returns。四个特性覆盖 4 种不同的日常痛点。
📌 C# 9 对应哪个 .NET?C# 9 随 .NET 5(2020.11)发布。但 .NET 5 不是 LTS(仅 18 个月支持),绝大多数项目跳过它直接上了 .NET 6 LTS(2021.11,自带 C# 10)。

C# 9 特性在 .NET 5、.NET 6、.NET 7、.NET 8 上全部可用——语言特性是前向兼容的。本课例子里出现 ".NET 6+" 是因为:① .NET 6 的默认模板才开始大规模采用 C# 9 特性(如顶级语句的 Minimal API);② .NET 6 是第一个统一后的 LTS,是生产环境的主流选择。

简单说:这些特性是 C# 9 的,可以用在任何 ≥ .NET 5 的项目上。本课用 .NET 6 举例是因为那才是你一上手就会看到的模板。

一、钩子——"Hello World" 为什么需要 12 行?

打开任何一个 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

二、顶级语句 Top-level Statements

2.1 什么能写?——就是你之前在 Main 里写的

// 完整示例:有 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 codereturn 0;
定义类 / struct / 方法✅ 放在语句之后
第二个文件也写顶级语句❌ 一个项目只能有一个文件用顶级语句

2.2 编译器生成了什么?——SDK 的魔法.NET 5+

你不是一直想问"编译器生成了什么"吗?来:

// 你写的(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);
    }
}
你不是要面试吗?Top-level statements 的面试常考题:"Program 类去哪了?"——答:编译器合成为一个 internal class Program,Main 方法改名叫 $Main。代码引用了 Program 类的地方(如 EF Core migration、ASP.NET Minimal API)照常工作——编译器的合成类和以前的 Program 合并。

2.3 什么时候用,什么时候不用?

✅ 适合❌ 不适合
控制台小工具、脚本、Demo大型项目——只有一个文件能省略,反而不一致
ASP.NET Core Minimal API 的 Program.cs需要显式 Main 签名(比如返回 Task<int>
.NET 6+ 默认模板——它就是默认团队有严格的代码风格规范要求显式定义

三、模式匹配增强 Pattern Matching Enhancements

C# 8 给了你 switch 表达式、属性模式、元组模式。C# 9 给模式加了三个逻辑关键字和一类比较操作——让模式匹配从"够用"到"顺手"。

3.1 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 而不是 != nullis not null 是模式匹配——编译器直接生成 IL 层面的 null 检查,不经过任何 operator != 重载。即使有人把 != 重载成奇怪的逻辑,is not null 也不受影响。这是最安全、最"确定性"的 null 检查方式。从 C# 9 开始,这是推荐的写法。

3.2 andor — 组合模式

// 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
};
记不住优先级?不用记——拆成两行或用 whenand 优先级高于 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# 表达式。

3.3 关系模式 Relational Patterns

关系模式就是把 >>=<<= 直接写进模式里面——不再需要 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,
};
关系模式 vs when:什么时候用谁?简单的比较运算(>=<)用关系模式——更短、更直观。涉及方法调用(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 不提供编译期类型信息

4.1 真实场景——这里最省事

场景C# 8C# 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)会自动建议这个——跟着改就行。

五、协变返回类型 Covariant Returns

这是本课最"低调但有用"的特性——你看一遍就会忘,但写工厂方法 / Builder / Fluent API 时会突然想起它。

5.1 旧的痛点

// 基类: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

5.2 规则很严格

规则说明
返回类型必须是引用类型只有 class / record 支持——值类型不行(struct 不支持协变)
必须是更具体的类型DogAnimal 的子类 ✅;反过来 AnimalDog 的子类 ❌
只对引用类型协变泛型参数不支持——List<Dog> 不能替代 List<Animal>(那是泛型协变的事)
隐式转换关系调用者拿基类引用时,返回值自动向上转型,完全透明

5.3 真实场景——Builder 模式的福音

// 一个典型的 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 小特性的真正价值:单个看都不大,放在一起用才看得出效果。日常写代码时,这四个特性几乎每个文件都会碰到。

📝 小测验

Q1. 关于 Top-level Statements,以下哪个说法是错误的?

选择不正确的描述:

Q2. is not null!= null 的关键区别是什么?

选择最准确的描述:

Q3. 以下模式匹配代码,哪个分支会先被匹配

score switch
{
    >= 80 => "B",    // ①
    >= 90 => "A",    // ②
    _     => "C",    // ③
};
score = 95 时,返回什么?

Q4. 以下哪个 new() 用法是合法的?

选择能编译通过的选项:

Q5. 关于协变返回类型,以下哪个说法是正确的?

选择正确的描述:

Q6.(场景题)你在写一个现代 .NET(6+)的 ASP.NET Core 应用。你想在 Program.cs 中同时使用顶级语句和显式的 Program 类(用于集成测试)。以下哪个做法是正确的?

选择最佳方案:

Q7. 关于 andor 组合模式,哪个描述是正确的?

选择正确的描述:

Lesson 08 · C# 9 小特性集 · 下一课预告:C# 10——Record Structs、Global Usings、File-scoped Namespaces、Constant Interpolated Strings

有任何不清楚的地方?直接在对话中追问——Agent 就是你的私教。