C# 8–11 重要特性总览
本教程共 100 篇 · 第 99 篇 · 更新于 2026-07-31 · 约 12 分钟阅读
99. C# 8–11 重要特性总览
本节目标:学完能一口气说出 C# 8 到 11 最核心的几个语法糖,知道各自解决什么痛点,并在自己代码里用上。
前面各章其实已经把这些特性拆开讲过。这里把它们并到一章,做个「巡礼」——方便你建立整体印象,也看看这门语言近几年的进化脉络。每个特性都配一小段代码,点到为止。
Note全书的基线就是 C# 14 / .NET 10,所以下面这些特性你现在就能直接用,不用任何开关。它们在 C# 8–11 陆续登场,早已成为现代 C# 的日常。
先给一张总表,一眼看清「谁、在哪版、治什么病」:
| 特性 | 版本 | 解决的核心痛点 |
|---|---|---|
| 可空引用类型 | C# 8 | 把「空引用异常」挡在编译期 |
| using 声明 | C# 8 | 砍掉释放资源的缩进样板 |
| 索引与范围 | C# 8 | 取倒数第几个、切片不再算长度 |
| 异步流 | C# 8 | 一边等 IO 一边逐个产出值 |
| record / init | C# 9 | 数据载体写起来太重 |
| 文件范围命名空间 | C# 10 | 少一层花括号缩进 |
| 全局 using | C# 10 | 不再每文件重复 using |
| 原始字符串字面量 | C# 11 | 多行/带引号文本免转义 |
| 必需成员 required | C# 11 | 编译期逼着填必填字段 |
一、可空引用类型(C# 8)
变量到底「可能为空」还是「一定不为空」,以前只能靠约定。C# 8 引入可空引用类型(nullable reference types),用 ? 显式标注:
string notNull = "一定不为空";
string? maybeNull = null; // 显式允许为空
// 若 maybeNull 没判空就直接用,编译器给警告
int len = maybeNull!.Length; // ! 表示「我保证它不为空」,慎用
配合 ?.(空条件)和 ??(空合并),把「空引用异常」挡在编译期。细节见第 90 章。
Tip
?只是给编译器的「注解」,不改变运行时行为——string?和string跑起来是同一种类型。它的价值在编译期提醒你「这里可能为空,先判空」。
为什么这样设计?因为「空引用」被图灵奖得主托尼·霍尔称为「十亿美元的错误」。C# 用「注解 + 编译警告」的方式,既兼容老代码,又把风险前移。注意它依赖可空上下文(nullable context):项目默认开启 #nullable enable,你也能用 #nullable disable 在某段关掉。建议长期保持开启,把它当免费的安全网。
Warning别滥用
!(空宽恕运算符)。它等于对编译器说「别管了,我保证非空」,一旦你保证错了,异常照旧在运行时炸。它该用于「你确实比编译器更清楚」的极少数场合。
二、using 声明(C# 8)
需要释放的资源(文件、连接、流),以前要包在大括号 using 块里。C# 8 的 using 声明(using declaration)改成「声明即生效,离开作用域自动 Dispose」:
using var reader = new StreamReader("data.txt");
string? line = reader.ReadLine();
// 方法结束,reader 自动释放,不必大括号缩进
代码更平、缩进更浅。第 93、95 章都是这么用的。
适用场景:任何实现了 IDisposable 的资源都该用它——文件、流、数据库连接、画笔等。和老式 using { } 块二选一,效果一样,只是声明式写法更省缩进。
三、索引与范围(C# 8)
取集合「倒数第几个」或「一段切片」,以前得算长度。C# 8 的索引(index)用 ^ 表示从末尾数,范围(range)用 .. 表示区间:
int[] numbers = [1, 2, 3, 4, 5]; // 集合表达式,C# 12,这里先借用
int last = numbers[^1]; // 5,^1 即倒数第一
int secondLast = numbers[^2]; // 4
int[] middle = numbers[1..4]; // [2, 3, 4],含头不含尾
int[] tail = numbers[2..]; // [3, 4, 5],省略尾到末尾
int[] head = numbers[..2]; // [1, 2],省略头从开头
.. 让「切片」表达得像数学区间一样直观。
对比旧写法:numbers[numbers.Length - 1](取末位) vs numbers[^1],哪个清爽一眼便知。它不止用于数组,string、List<T>、Span<T> 都支持。第 20、26 章有更细的玩法。
四、异步流(C# 8)
一个方法要「一个一个产出多个值」,且每个值可能要等 IO——这就是异步流(async stream),返回 IAsyncEnumerable<T>,用 await foreach 消费:
async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(100); // 模拟异步等待
yield return i;
}
}
// 消费端
await foreach (var n in GetNumbersAsync())
{
Console.WriteLine(n);
}
yield return 让方法「暂停、给一个值、再继续」。配合 await foreach,读起来和同步 foreach 一样顺。第 61、83 章有铺垫。
适用场景:读大文件逐行处理、逐页拉取接口数据、实时消息推送——凡是「边产出边消费、且中间要等 IO」的,它比「先攒齐再返回 List」省内存也更自然。
五、record 与 init 访问器(C# 9)
值相等、不可变的数据载体,用 class 写很啰嗦。C# 9 的 record(记录类型)一行搞定,自带值相等比较:
record Person(string Name, int Age); // 位置记录的主构造器(即这里的位置参数)来自 C# 9;C# 12 才把主构造器扩展到普通 class / struct
var a = new Person("张三", 18);
var b = new Person("张三", 18);
Console.WriteLine(a == b); // True:按值相等,不是按引用
var older = a with { Age = 19 }; // with 表达式复制并改字段
init 访问器让属性「创建时可赋值、之后只读」:
class Student
{
public string Name { get; init; } // 只能在对象初始化器里设一次
public int Score { get; set; }
}
record 与 init 详见第 49、35 章。
为什么这样设计?数据传输对象(DTO)、配置、坐标点这类「只装数据、比的是内容」的类型,用普通 class 要手写一堆 Equals/GetHashCode 才方便比较。record 把这些 boilerplate 全收进编译器,还顺手给了不可变语义——数据一旦创建就别乱改,多线程下更安全。
六、文件范围命名空间(C# 10)
以前命名空间(namespace)要花括号把整文件包起来。C# 10 的文件范围命名空间(file-scoped namespace)用一行就够,全文件都在这个命名空间下:
namespace CSharpDemo; // 后面所有类型都归属于它,不用缩进
class Demo { }
本书所有示例都这么写。它纯粹是语法糖,编译结果和花括号写法一致,但省一层缩进、更清爽。
Tip极少数场景仍需花括号写法:一个文件里要放两个命名空间时(不推荐,但 legacy 代码里有)。日常一律用文件范围,干净。
七、全局 using(C# 10)
using System; 这种几乎每个文件都要的引用,C# 10 可以提升到「全局」一次声明,全项目生效:
global using System;
global using System.Collections.Generic;
配合项目里的 <ImplicitUsings>enable</ImplicitUsings>,连写都不用写——这就是前面章节「不再手写 using」的来历。需要时仍可在文件里局部 using 特定命名空间。
适用场景:团队统一的、全项目都要的基础命名空间(如 System、System.Linq)。但别把「所有 using 都 global 化」,只放真正人人都用到的,否则名字冲突反而变多。
八、原始字符串字面量(C# 11)
含引号、换行、JSON 的多行文本,以前要满屏转义。C# 11 的原始字符串字面量(raw string literal)用三个双引号 """ 括起,内部原样保留:
string json = """
{
"name": "张三",
"age": 18
}
""";
string quoted = """他说:"你好"。"""; // 内部引号无需转义
缩进以「结尾 """ 所在列」为基准自动去除,所以代码好看、内容干净。第 13 章专门讲过。
对比旧式逐字字符串 @"...":遇到内部有 " 还得写成 "",多行更要拼。原始字符串把这些都免了,写 SQL、JSON、HTML 模板尤其舒服。
九、必需成员(C# 11)
对象初始化时,如何逼着调用方「必须填某些字段」?C# 11 的 required 修饰符做编译期强制:
class Order
{
public required int Id { get; init; }
public required string Customer { get; init; }
public string Note { get; set; } = "";
}
// 漏掉任一 required,编译直接报错
var o = new Order { Id = 1, Customer = "李四" };
漏写 Id 或 Customer,编译器当场拦下,比运行时才发现 null 稳得多。第 91 章结合主构造器讲过它。
Note关于「集合表达式(collection expressions)」:大纲把它列在本章,但它在语言上属于 C# 12(用
[1, 2, 3]初始化各种集合)。为避免版本口径混乱,本书把它归入下一章「C# 12–14 新特性总览」统一讲解,第 26 章也已先行演示过[...]写法。
十、这一时期的脉络
把 8–11 串起来看,主线很清楚:
- 更安全:可空引用类型把空检查前移到编译期。
- 更简洁:using 声明、文件范围命名空间、全局 using、原始字符串,都在削减样板。
- 更声明式:索引范围、
record、required,让「意图」直接写在代码里。
这些特性彼此呼应,共同把 C# 推向「少写废话、多写逻辑」的现代风格。
值得注意的是,这条进化线并不是「为了新而新」。可空引用类型解决的是困扰 C# 二十年的「空引用异常」;using 声明和文件范围命名空间解决的是「样板代码淹没业务逻辑」;record 与 required 解决的是「数据载体写起来太重」;异步流则让「一边等 IO 一边产出」第一次有了原生、好看写法。每一项都对应一个真实痛点。你写代码时若发现「这里好啰嗦、好容易错」,多半已经有对应语法糖在等你——回头翻这章,往往能找到答案。
十一、本节小结
C# 8–11 是这门语言「现代化」的关键几年:可空引用提升安全,using 声明与命名空间简化结构,索引范围与异步流增强表达力,record/init 与 required 让数据载体更优雅。下一章我们冲到 C# 12–14,看这门语言最新的几记「重拳」。
动手练一练
- 用
record定义一个Point(int X, int Y),验证两个字段相同的实例==为True。 - 用文件范围命名空间 + 全局 using 改写你之前的一个小程序,体会「少写 using」的差异。
- 用
required定义一个User类,故意漏掉一个必需成员,观察编译器是否报错。