C# · #csharp#language-features#dotnet-core

C# 8.0 新特性:可空引用类型与模式匹配

2019.04.08 C# 4 min 1.4k
// 目录 · contents

C# 8.0 和 .NET Core 3.0 同步发布(预览版已经可以用),这个版本最令人期待的是可空引用类型(Nullable Reference Types)——这是 C# 自诞生以来对 null 问题最彻底的一次正面回应。

1. 可空引用类型(Nullable Reference Types)

C# 历来的痛点:引用类型默认可以是 null,但编译器没有任何警告,运行时才抛 NullReferenceException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 启用可空引用类型(在项目文件或文件顶部开启)
// .csproj: <Nullable>enable</Nullable>
// 或文件内: #nullable enable

string name = null; // ⚠ 警告:不能把 null 赋给不可空的 string
string? nullableName = null; // ✅ 明确声明可以为 null

// 使用可空类型时必须先检查
void PrintName(string? name)
{
// 直接使用会警告
Console.WriteLine(name.Length); // ⚠ 警告:name 可能为 null

// 正确方式 1:null 检查
if (name != null)
{
Console.WriteLine(name.Length); // ✅ 编译器知道这里不为 null
}

// 正确方式 2:null 条件运算符
Console.WriteLine(name?.Length ?? 0);

// 正确方式 3:null 断言运算符(告诉编译器"我保证不为 null")
Console.WriteLine(name!.Length); // 绕过检查,自己负责
}

在现有项目中逐步启用:

1
2
3
4
5
6
7
8
// 文件级别控制,逐步迁移
#nullable enable
public class UserService
{
public User? FindById(int id) { ... } // 可能返回 null
public User GetOrThrow(int id) { ... } // 保证不返回 null
}
#nullable restore

这个特性是静态分析,不影响运行时行为,只是让编译器帮你提前发现潜在的 NullReferenceException。

2. 增强的模式匹配

C# 7.0 引入了基础模式匹配,8.0 大幅增强:

Switch 表达式(替代 switch 语句)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 旧写法(switch 语句)
string GetDiscount(UserLevel level)
{
switch (level)
{
case UserLevel.Bronze: return "5%";
case UserLevel.Silver: return "10%";
case UserLevel.Gold: return "15%";
default: return "0%";
}
}

// C# 8.0(switch 表达式,更简洁)
string GetDiscount(UserLevel level) => level switch
{
UserLevel.Bronze => "5%",
UserLevel.Silver => "10%",
UserLevel.Gold => "15%",
_ => "0%" // 默认情况
};

属性模式

1
2
3
4
5
6
7
8
9
10
// 根据对象属性匹配
decimal CalculateShipping(Order order) => order switch
{
{ Weight: < 1, IsExpress: false } => 5.00m,
{ Weight: < 5, IsExpress: false } => 10.00m,
{ Weight: >= 5, IsExpress: false } => 15.00m,
{ IsExpress: true, Weight: < 1 } => 15.00m,
{ IsExpress: true } => 25.00m,
_ => throw new ArgumentException("Invalid order")
};

元组模式

1
2
3
4
5
6
7
8
// 多个值的组合匹配
string GetTrafficLight(int hour, bool isWeekend) => (hour, isWeekend) switch
{
(>= 7 and <= 9, false) => "红灯(早高峰)",
(>= 17 and <= 19, false) => "红灯(晚高峰)",
(_, true) => "绿灯(周末)",
_ => "正常通行"
};

位置模式(解构匹配)

1
2
3
4
5
6
7
8
9
10
public record Point(int X, int Y);

string ClassifyPoint(Point p) => p switch
{
(0, 0) => "原点",
(var x, 0) when x > 0 => "正 X 轴",
(0, var y) when y > 0 => "正 Y 轴",
(var x, var y) when x > 0 && y > 0 => "第一象限",
_ => "其他"
};

3. 异步流(Async Streams)

这是 .NET Core 3.0 的重大特性,终于支持 async IAsyncEnumerable<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生产者:异步生成数据流
public async IAsyncEnumerable<StockPrice> GetPriceUpdatesAsync(
string symbol,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
var price = await _api.GetPriceAsync(symbol);
yield return price;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}

// 消费者:await foreach
await foreach (var price in GetPriceUpdatesAsync("AAPL", cts.Token))
{
Console.WriteLine($"AAPL: {price.Value}");
}

之前要实现类似功能需要用 IObservable<T> + Rx.NET,现在语言层面直接支持了。

4. using 声明(简化资源释放)

1
2
3
4
5
6
7
8
9
10
11
// 旧写法
using (var reader = new StreamReader(path))
{
var content = reader.ReadToEnd();
// ...
} // reader 在这里释放

// C# 8.0 using 声明(在作用域结束时自动释放)
using var reader = new StreamReader(path);
var content = reader.ReadToEnd();
// reader 在方法结束时释放,无需嵌套缩进

5. 接口默认实现(谨慎使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface ILogger
{
void Log(string message);

// 8.0 新增:接口方法可以有默认实现
void LogWarning(string message)
{
Log($"[WARNING] {message}");
}

void LogError(string message)
{
Log($"[ERROR] {message}");
}
}

// 实现类只需要实现 Log 方法
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
// LogWarning/LogError 使用默认实现
}

个人建议:谨慎使用接口默认实现。它主要用于解决接口演化问题(给现有接口加方法而不破坏实现类),不应该作为常规设计手段——那是抽象类的职责。

总结

C# 8.0 最值得立即使用的特性:

特性 价值 迁移成本
可空引用类型 消灭 NullReferenceException 需要逐步修复警告
Switch 表达式 简洁的条件分支 低,可逐步替换
属性/元组模式 复杂条件更可读
异步流 真正的异步迭代 需要 .NET Core 3.0+
using 声明 减少嵌套 极低

可空引用类型是这个版本最重要的特性,虽然开启后需要处理一些警告,但长远来看能显著减少线上 NullReferenceException 的数量。建议新项目直接开启,老项目按模块逐步启用。

在这之后不久,我开始把更多精力转向 Java 生态(主要原因是公司主要技术栈迁移),但 C# 这些年积累的开发习惯——特别是强类型、LINQ、async/await——对后来学习 Java 的很多概念很有帮助。

作者 · authorzt
发布 · date2019-04-08
篇幅 · length1.4k 字 · 4 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论