Chapter 15. C# 8 and beyond
以第四版2019年的preview视角覆盖nullable references、switch expressions、recursive patterns、indexes/ranges、async streams/disposal及参与语言演进的方法。
学习目标
- 能解释nullable reference annotations与flow analysis如何表达null intent,区分warning、suppression、runtime validation和migration context
- 能推导switch expression、recursive patterns、Index与Range的match/bounds结果,并设计exhaustiveness和slice-cost tests
- 能设计async stream与async disposal的producer-consumer protocol,复现iteration、early exit、cancellation、failure和cleanup路径
机制总览
Chapter 15. C# 8 and beyond:机制路径
- 1
为什么必须同时保留Preview历史与最终Version边界
第四版在2019年出版,本章面对的是C 8尚在形成中的language。它不仅介绍syntax,也示范如何读proposal、试preview compiler、区分design intent与shipping guarantee。今天回看时要做双层叙述:先还原书中看到的候选方向,再标明后来C 8落地的稳定语义;不能把更晚版本的扩展伪装成作者当时已知结论。
- 2
Nullable reference types
Nullable reference types为reference values增加compile-time intent:在enabled context中, string 表示正常path期望non-null, string?
- 3
Switch expressions
Switch expression把input映射为一个result,arms通常以pattern、optional guard和expression组成。它比statement更适合pure classification,促使每个arm产生同一target-compatible result。Ar…
章级决策实验
Chapter 15. C# 8 and beyond:机制与证据
切换《Chapter 15. C# 8 and beyond》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么必须同时保留Preview历史与最终Version边界
第四版在2019年出版,本章面对的是C 8尚在形成中的language。它不仅介绍syntax,也示范如何读proposal、试preview compiler、区分design intent与shipping guarantee。今天回看时要做双层叙述:先还原书中看到的候选方向,再标明后来C 8落地的稳定语义;不能把更晚版本的扩展伪装成作者当时已知结论。
可核验证据
以明确的 LangVersion 与目标框架构建「为什么必须同时保留Preview历史与最终Version边界」的正反案例,并用编译诊断、生成 IL、运行轨迹或分配数据核对实际边界。
学完《Chapter 15. C# 8 and beyond》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 15. C# 8 and beyond:失效与核验
为什么必须同时保留Preview历史与最终Version边界
典型失效
若解释「为什么必须同时保留Preview历史与最终Version边界」时混淆语言规范、编译器降级、运行时和类库责任,版本变化后就会把实现细节误当作 C# 语义保证。
核验证据
以明确的 LangVersion 与目标框架构建「为什么必须同时保留Preview历史与最终Version边界」的正反案例,并用编译诊断、生成 IL、运行轨迹或分配数据核对实际边界。
Nullable reference types
典型失效
若解释「Nullable reference types」时混淆语言规范、编译器降级、运行时和类库责任,版本变化后就会把实现细节误当作 C# 语义保证。
核验证据
以明确的 LangVersion 与目标框架构建「Nullable reference types」的正反案例,并用编译诊断、生成 IL、运行轨迹或分配数据核对实际边界。
Switch expressions
典型失效
若解释「Switch expressions」时混淆语言规范、编译器降级、运行时和类库责任,版本变化后就会把实现细节误当作 C# 语义保证。
核验证据
以明确的 LangVersion 与目标框架构建「Switch expressions」的正反案例,并用编译诊断、生成 IL、运行轨迹或分配数据核对实际边界。
为什么必须同时保留Preview历史与最终Version边界
第四版在2019年出版,本章面对的是C# 8尚在形成中的language。它不仅介绍syntax,也示范如何读proposal、试preview compiler、区分design intent与shipping guarantee。今天回看时要做双层叙述:先还原书中看到的候选方向,再标明后来C# 8落地的稳定语义;不能把更晚版本的扩展伪装成作者当时已知结论。
先预测:string?是否创建新的CLR runtime type;!是否插入runtime null check;switch expression是否天然处理所有未来subtypes;^1是否等于length-1这个数值而不依赖receiver;range slicing是否总为零allocation;停止await foreach是否自动取消所有producer work。答案都需要contract和target-type context,而不是只背符号。
Nullable reference types
Nullable reference types为reference values增加compile-time intent:在enabled context中,string表示正常path期望non-null,string?表示null是允许state。它不改变CLR中reference可为null的事实,也不会让string成为全新runtime type;价值来自annotations、warnings和flow analysis共同暴露潜在contract mismatch。
Flow state会因null check、patterns、assignment、member annotations和method contracts变化。Migration应按boundary推进:先启用warnings观察legacy oblivious code,再标annotated API,修真正fault,最后才对有外部证明的点使用suppression。value!只告诉compiler暂信此处non-null,runtime value完全不变。
#nullable enable
static int LengthOrZero(string? text)
{
if (text is null) return 0;
return text.Length; // flow state is non-null here
}切换non-null、maybe-null、oblivious与suppressed
Switch expressions
Switch expression把input映射为一个result,arms通常以pattern、optional guard和expression组成。它比statement更适合pure classification,促使每个arm产生同一target-compatible result。Arm order仍重要,missing case需要明确处理;对enum或closed hierarchy可用tests/analysis逼近exhaustiveness,对open world必须保留unknown policy。
static decimal Discount(Customer customer) => customer switch
{
{ IsVip: true, Years: >= 5 } => 0.20m,
{ IsVip: true } => 0.10m,
null => throw new ArgumentNullException(nameof(customer)),
_ => 0m,
};Recursive pattern matching
Recursive patterns允许在一个pattern中继续检查properties、positions与nested shapes,把“type + fields + guards”写成declarative classifier。C# 8的property/positional patterns建立在Chapter 12的test-and-bind model上;后续版本又加入relational、logical、list等patterns,必须另标版本,不能塞回原书preview。
Pattern读取property可能执行getter并抛异常或产生cost,不能假设它只是field match。Overlapping arms按order解决;复杂business classification应配decision table,若pattern树比domain language更难读,就抽出named predicates或domain methods。
↡Pattern对一个value的nested properties或deconstructed positions继续应用subpatterns的shape test。Indexes and ranges
Index表达从start或end定位,^1通常表示last element;Range用start..end表达start-inclusive、end-exclusive interval。语法由receiver支持的indexer/Length/Count与Slice patterns解释,因此bounds、empty range和copy/view semantics取决于target type。
Array/string slicing常产生新array/string;Span slicing通常产生view。不要从[..]符号推断allocation-free。Off-by-one tests应覆盖empty、single、full、from-end、start=end、reversed与out-of-range,并明确Unicode string slicing按UTF-16 code units而非grapheme clusters。
切换switch、property pattern、index与range
More async integration
C# 8把async扩展到iteration与cleanup。IAsyncEnumerable<T>配合await foreach让每次MoveNext可异步等待,适合streaming I/O;IAsyncDisposable配合await using让cleanup本身可异步完成。它们不是把普通collection/Dispose加一个await,而是新的producer-consumer protocols。
await foreach (Order order in repository.StreamAsync(cancellationToken))
{
await ProcessAsync(order, cancellationToken);
}
await using var lease = await pool.AcquireAsync(cancellationToken);Early break、consumer exception、cancellation和enumerator failure都必须走cleanup policy。Cancellation token如何传给async iterator取决于method/consumer contract;停止enumeration不一定终止producer已启动的independent work。Backpressure也不是自动无限安全,producer每次yield和buffering strategy要审计。
↡Producer异步提供sequence elements,consumer逐次await move并在终止时可靠释放enumerator的协议。 ↡Resource cleanup本身需要await,并在normal、failure、cancellation和early-exit paths都被完成的协议。切换async stream、dispose、cancellation与failure
Features not yet in preview
原书把“尚未进入preview”的候选特性单独讨论,核心方法比名单更重要:proposal可能被推迟、重塑、拆分或取消;syntax、runtime support、BCL support和tooling maturity也可能不同步。教材应保存当时状态,不把后来结果倒写成必然。
阅读历史proposal时给每项加四列:book-time status、first shipping version、current semantics、required runtime/library。这样既尊重原书,也让现代reader不把preview sample直接复制到production。
Getting involved
C#设计过程公开在language proposals、notes、compiler preview与issue discussions中。有效参与不是只投票喜欢某个syntax,而是提交最小use case、alternative、compatibility constraints、lowering/runtime cost和可复现compiler feedback。先验证已有proposal是否覆盖问题,再用小project测试LangVersion和target framework。
参与也要区分language、compiler、runtime与library repositories。一个feature无法工作时,先缩小是parser/type checker、generated IL、runtime capability还是BCL API缺口;清晰的层次证据比宽泛意见更能推动设计。
本章回顾:Preview阅读能力也是语言能力
- Nullable references增加annotation和flow analysis,不改变CLR null,也不替代runtime validation。
- Switch expressions与recursive patterns压缩classification,但仍依赖order、coverage和getter semantics。
- Index/Range表达bounds,allocation与view由target type决定。
- Async streams/disposal扩展了iteration与cleanup protocols,cancellation和failure ownership必须测试。
- Preview、shipping与modern extension要分层记录;参与设计需要可复现use case和跨层证据。
练习
问题 1:一个legacy library启用nullable后出现五千warnings,怎样迁移?
问题 2:Range API如何证明既正确又没有错误的性能假设?
问题 3:Async stream在consumer处理第3项失败时怎样闭环?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- nullable flow state
- exhaustiveness policy
- recursive shape match
- slice boundary contract
- asynchronous iteration protocol
- asynchronous cleanup ownership
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Recursive pattern matching:机制、边界与证据
Chapter 15. C# 8 and beyond中的Recursive pattern matching涉及值的存储位置、复制语义与生命周期,语法简洁不代表没有别名或逃逸限制。准备可编译和应被编译器拒绝的样本,结合 IL、分配计数或地址/修改轨迹核对复制、别名与边界。
Indexes and ranges:机制、边界与证据
Chapter 15. C# 8 and beyond中的Indexes and ranges必须分清语言规范、编译器实现、运行时行为与基础类库 API 四层责任。固定 C# 语言版本和目标框架,用正向/负向编译案例、必要的 IL 或运行轨迹以及版本对照验证结论。
More async integration:机制、边界与证据
Chapter 15. C# 8 and beyond中的More async integration涉及值的存储位置、复制语义与生命周期,语法简洁不代表没有别名或逃逸限制。准备可编译和应被编译器拒绝的样本,结合 IL、分配计数或地址/修改轨迹核对复制、别名与边界。
Features not yet in preview:机制、边界与证据
Chapter 15. C# 8 and beyond中的Features not yet in preview涉及值的存储位置、复制语义与生命周期,语法简洁不代表没有别名或逃逸限制。准备可编译和应被编译器拒绝的样本,结合 IL、分配计数或地址/修改轨迹核对复制、别名与边界。
Getting involved:机制、边界与证据
Chapter 15. C# 8 and beyond中的Getting involved涉及值的存储位置、复制语义与生命周期,语法简洁不代表没有别名或逃逸限制。准备可编译和应被编译器拒绝的样本,结合 IL、分配计数或地址/修改轨迹核对复制、别名与边界。