Lesson 12: C# 11 — 列表模式 + 必需成员

用模式匹配描述集合形状 · 用编译期检查替代运行时报错

前置:已完成 Lesson 11(Raw String Literals);理解 模式匹配Span/ReadOnlySpan
本课:List Patterns + Required Members。UTF-8 String Literals 在 Lesson 13

第一部分:列表模式 List Patterns

一、痛点——检查数组前三个元素?写一大堆

😣 C# 10

if (arr.Length == 3
    && arr[0] == 1
    && arr[1] >= 10
    && arr[2] <= 100) { }

😎 C# 11

if (arr is [1, >= 10, <= 100]) { }

方括号里的不是值——是模式。这就是 List Patterns:用模式匹配来解构和检查集合。

二、完整语法

// ① 精确匹配——长度和元素都相等
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr is [1, 2, 3]);   // True
Console.WriteLine(arr is [1, 2]);        // False——长度不匹配

// ② 元素使用任意模式(常量、关系、类型、属性……)
if (arr is [not 0, ..]) { }  // 第一项不是 0

// ③ 切片模式(..)——匹配任意数量元素(含零个)
if (arr is [1, ..]) { }          // 第一个是 1,后面随便
if (arr is [.., 9]) { }          // 最后一个是 9
if (arr is [1, .., 9]) { }        // 第一个 1,最后 9

// ④ 嵌套模式——元素是对象时匹配属性
if (orders is [{ Status: "New" }, .., { Total: >= 1000 }]) { }

// ⑤ var 捕获——提取匹配到的元素
if (arr is [var first, .. var middle, var last])
{ // first=arr[0], middle=arr[1..^1], last=arr[^1] }

三、鸭子类型——不要求实现 IList

列表模式不要求实现特定接口。编译器只检查:是否有 Length 或 Count 属性 + 可访问索引器。支持的类型:

类型支持?说明
int[] / T[]数组原生支持
List<T>有 Count + 索引器
Span<T> / ReadOnlySpan<T>C# 11 新增——Span 模式匹配
string有 Length + char 索引器
自定义集合满足鸭子类型条件即可

四、switch 表达式——最自然的用法

static string Describe(int[] arr) => arr switch
{
    []                    => "空数组",
    [0]                   => "只有一个 0",
    [var x, var y]           => $"两个元素:{x}, {y}",
    [1, .., 1]              => "首尾都是 1",
    [.., 0]                => "最后一个元素是 0",
    [not 0, ..]             => "第一个不是 0",
    _                     => "其他",
};

// 实战:REST API 路由解析
static string ParseRoute(string[] segments) => segments switch
{
    ["api", "users"]             => "用户列表",
    ["api", "users", var id]    => $"用户详情 {id}",
    ["api", "users", var id, "orders"] => $"用户 {id} 的订单",
    _                            => "未知",
};

第二部分:必需成员 Required Members

五、痛点——"这个属性必须设"靠嘴说

😣 C# 10

public class Person
{
    public string Name { get; set; }
}
// 忘了设 Name?运行时才发现为 null
var p = new Person();  // 😱 编译器没意见

😎 C# 11

public class Person
{
    public required string Name { get; set; }
}
// ❌ CS9035:Name is required
var p = new Person { Name = "Alice" };  // ✅

required 关键字将"必填属性"从运行时报错变成编译时错误。IDE 在你没写完代码时就显示红色波浪线。

六、两条赋值路径

new Person() ← 你 new 了一个对象 │ ├── 路径 A:对象初始化器 │ new Person { Name = "A", Email = "x@y.com" } │ 编译器比对:required 成员都出现了吗? │ └── 路径 B:构造函数体内赋值 构造函数有 [SetsRequiredMembers] → 编译器放行 构造函数无 [SetsRequiredMembers] → 调用者必须用初始化器补齐

路径 A:纯对象初始化器

public class Person
{
    public required string Name { get; set; }
    public required string Email { get; set; }
    public int Age { get; set; }  // 没有 required——可选
}

var p1 = new Person { Name = "Alice", Email = "a@b.com" };  // ✅
var p2 = new Person { Name = "Alice" };                  // ❌ CS9035: Email 未赋值
var p3 = new Person();                                     // ❌ CS9035: Name 和 Email 都未赋值

路径 B:构造函数 + [SetsRequiredMembers]

public class Person
{
    public required string Name { get; set; }
    public required string Email { get; set; }

    // 有 SetsRequiredMembers → 编译器信任此构造函数
    [SetsRequiredMembers]
    public Person(string name, string email)
    {
        Name = name;
        Email = email;
    }

    // 无 SetsRequiredMembers → 调用者必须用初始化器补齐 Email
    public Person(string name)
    {
        Name = name;
        // Email 未赋值 → required 悬空
    }
}

var p1 = new Person("Alice", "a@b.com");  // ✅ 无需初始化器
var p2 = new Person("Alice") { Email = "a@b.com" };  // ✅ 必须补齐
var p3 = new Person("Alice");  // ❌ CS9035: Email 悬空
[SetsRequiredMembers] 是承诺,不是验证。编译器不检查构造函数是否真的设置了所有 required 成员——你承诺了,编译器就信你。这是一个"信任程序员"的契约。

七、常用模式——混合路径

// 构造函数设默认值,初始化器补 required 差额——最常见模式
public class ApiClient
{
    public required string BaseUrl { get; set; }
    public required string ApiKey { get; set; }
    public int Timeout { get; set; }

    public ApiClient() { Timeout = 30; }  // 不加 SetsRequiredMembers
}

var client = new ApiClient { BaseUrl = "https://...", ApiKey = "sk-xxx" };
// ✅ Timeout=30(构造函数),BaseUrl 和 ApiKey(初始化器)

八、Required + init = 编译期强制不可变

public class ApiConfig
{
    public required string BaseUrl { get; init; }   // 必须设值,设了就不能改
    public required string ApiKey { get; init; }
    public int Timeout { get; init; } = 30;       // 有默认值——不需要 required
}
required 是纯粹编译时检查——IL 中没有运行时代码。编译后 required 属性就是普通属性,所有检查在编译时完成。

九、小测验

1/4 · int[] { 1, 2, 3, 4, 5 } 能被哪个模式匹配?
2/4 · List Patterns 只支持数组和 List<T>?
3/4 · [SetsRequiredMembers] 做了什么?
4/4 · 关于 required 的 IL——正确的是?

十、下一步

💬 有问题?List Patterns 的 slice 语法不清楚?Required 的赋值路径有困惑?随时问。