【.NET新特性·第7篇】.NET 9 库与 LINQ 新增 API

CountBy、AggregateBy、Base64Url、Task.WhenEach、Lock 类型------.NET 9 标准库带来 20+ 实用 API

版本定位

适用版本:.NET 9 前置知识:LINQ 基础、集合操作

背景

.NET 9 不只是 C# 语言的更新,标准库也带来了一大批实用的新 API。从 LINQ 的 CountBy/AggregateBy 到安全领域的 BinaryFormatter 移除,从高性能的 SearchValues 增强到并发编程的 Task.WhenEach,这些 API 解决了长期以来的痛点,让代码更简洁、性能更好、安全性更强。

新增 API 一览

API 命名空间 用途 实用性
CountBy System.Linq 按键计数,无需 GroupBy ⭐⭐⭐⭐⭐
AggregateBy System.Linq 按键聚合,无需 GroupBy ⭐⭐⭐⭐⭐
Index System.Linq 为元素添加索引 ⭐⭐⭐⭐
Base64Url System.Buffers URL 安全的 Base64 编解码 ⭐⭐⭐⭐⭐
Task.WhenEach System.Threading.Tasks 按完成顺序获取 Task 结果 ⭐⭐⭐⭐⭐
BinaryFormatter 移除 --- 安全:完全移除危险的序列化器 ⭐⭐⭐⭐⭐
Lock 类型 System.Threading 新的轻量级线程同步原语 ⭐⭐⭐⭐
OrderedDictionary<TKey,TValue> System.Collections.Generic 有序字典 ⭐⭐⭐⭐
Guid v7 System 时间有序的 GUID ⭐⭐⭐⭐
ServerSentEvents System.Net.ServerSentEvents SSE 编解码 ⭐⭐⭐⭐
SearchValues 增强 System.Buffers 更高性能的搜索 ⭐⭐⭐⭐
FrozenDictionary 增强 System.Collections.Frozen 批量添加、TryCreateFrozen ⭐⭐⭐⭐
Tensor<T> System.Numerics.Tensors AI 计算基块 ⭐⭐⭐
PriorityQueue.Remove System.Collections.Generic 从队列中移除元素 ⭐⭐⭐⭐
Parallel.ForEachAsync System.Threading.Tasks 异步并行ForEach ⭐⭐⭐⭐
Span/ReadOnlySpan 改进 System 隐式转换、新方法 ⭐⭐⭐⭐
System.Text.Json 改进 System.Text.Json 枚举值字符串、UnmappedMemberHandling ⭐⭐⭐⭐
CryptographicOperations System.Security.Cryptography 一次性哈希/验证 ⭐⭐⭐⭐
StringSyntax 增强 System.Diagnostics.CodeAnalysis 更多语法标记 ⭐⭐⭐
Polyfill 支持 System.Runtime.CompilerServices 内置 Polyfill ⭐⭐⭐
TimeSpan 从整数创建 System 避免浮点精度问题 ⭐⭐⭐
PersistedAssemblyBuilder System.Reflection.Emit 可保存的动态程序集 ⭐⭐⭐
Stopwatch 改进 System.Diagnostics GetTimestamp 高精度计时 ⭐⭐⭐

一、LINQ 新方法详解

1. CountBy ------ 按键计数

之前的做法:GroupBy + Count

复制代码
// .NET 8 及之前
var counts = list
    .GroupBy(x => x.Category)
    .Select(g => new { Category = g.Key, Count = g.Count() })
    .ToDictionary(x => x.Category, x => x.Count);
// { "A": 3, "B": 2, "C": 1 }

.NET 9 的做法:一行搞定

复制代码
var counts = list.CountBy(x => x.Category);
// { "A": 3, "B": 2, "C": 1 }
对比 GroupBy + Count CountBy
代码行数 3-4 行 1 行
中间分配 多次
返回类型 Dictionary IEnumerable<KeyValuePair>

2. AggregateBy ------ 按键聚合

复制代码
// 旧做法
var totals = list
    .GroupBy(x => x.Type)
    .Select(g => new { Type = g.Key, Total = g.Sum(x => x.Amount) });

// .NET 9
var totals = list.AggregateBy(
    keySelector: x => x.Type,
    seed: 0m,
    accumulator: (acc, item) => acc + item.Amount
);

更复杂的聚合

复制代码
var stats = list.AggregateBy(
    keySelector: x => x.Category,
    seed: new { Total = 0m, Count = 0 },
    accumulator: (acc, item) => new
    {
        Total = acc.Total + item.Amount,
        Count = acc.Count + 1
    }
);

3. Index ------ 添加索引

复制代码
// 旧做法
var indexed = list.Select((item, index) => new { Index = index, Item = item });

// .NET 9
var indexed = list.Index();
foreach (var (index, item) in indexed)
{
    Console.WriteLine($"{index}: {item}");
}

二、高性能 API

4. Base64Url 编解码

痛点 :URL 中不能直接用标准 Base64(+/= 有特殊含义)

复制代码
// 旧做法:手动替换
string ToBase64Url(byte[] data)
{
    return Convert.ToBase64String(data)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
}

// .NET 9:内置支持
string encoded = Base64Url.EncodeToString(data);
byte[] decoded = Base64Url.FromBase64String(encoded);

// Span 版本(零分配)
int written = Base64Url.EncodeToUtf8(data, buffer);

典型场景:JWT token、URL 安全的 ID、加密数据传输

5. SearchValues 增强

复制代码
// .NET 8:只支持字符和字节
var searchValues = SearchValues.Create("abc");

// .NET 9:支持更多类型
var intSearch = SearchValues.Create(new[] { 1, 2, 3 });
var utf8Search = SearchValues.Create("hello"u8);

// 使用
ReadOnlySpan<int> data = [1, 2, 3, 4, 5, 1, 2, 3];
int index = data.IndexOfAny(intSearch); // 返回 0

6. FrozenDictionary / FrozenSet 增强

复制代码
// .NET 8:只能从 Dictionary 创建
var frozen = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 }
    .ToFrozenDictionary();

// .NET 9:更多创建方式
var frozen2 = FrozenDictionary.ToFrozenDictionary(
    [("a", 1), ("b", 2)]
);

// TryCreateFrozen(批量添加)
var result = FrozenDictionary.TryCreateFrozen(
    [("a", 1), ("b", 2), ("a", 3)],  // 重复键
    EqualityComparer<string>.Default,
    out var frozen3  // 只保留第一个 "a"
);

7. Span / ReadOnlySpan 改进

复制代码
// .NET 9 新增的隐式转换
Span<int> span = new int[] { 1, 2, 3 };  // 数组到 Span 隐式转换

// 新增方法
ReadOnlySpan<char> span = "Hello, World!";
span.ContainsAny("aeiou");  // 检查是否包含任一字符
span.ContainsAny("aeiou".AsSpan());

三、并发与异步

8. Task.WhenEach ------ 按完成顺序

痛点Task.WhenAll 等所有完成才返回,无法按完成顺序处理

复制代码
// 旧做法
var results = await Task.WhenAll(task1, task2, task3);
// 全部完成后才能处理

// .NET 9:按完成顺序逐个处理
await foreach (var task in Task.WhenEach(task1, task2, task3))
{
    var result = await task;
    Console.WriteLine($"完成: {result}");
    // 第一个完成的任务立即处理,不等其他
}

典型场景:并发 HTTP 请求、批量任务处理、实时日志收集

9. Parallel.ForEachAsync 改进

复制代码
// .NET 8
await Parallel.ForEachAsync(items, new ParallelOptions
{
    MaxDegreeOfParallelism = 4
}, async (item, ct) =>
{
    await ProcessAsync(item, ct);
});

// .NET 9:更简洁
await Parallel.ForEachAsync(items, async (item, ct) =>
{
    await ProcessAsync(item, ct);
});
// 自动选择最优并行度

10. Lock 类型

痛点lock 语句使用 Monitor,无法跨 await 使用

复制代码
// .NET 8 及之前
private readonly object _lock = new object();
lock (_lock)
{
    // 不能 await
}

// .NET 9:新的 Lock 类型
private readonly Lock _lock = new Lock();

// 在同步代码中使用
lock (_lock)
{
    _counter++;
}

// 在异步代码中使用
using (await _lock.EnterAsync())
{
    await UpdateAsync();
}

四、集合改进

11. OrderedDictionary<TKey, TValue>

复制代码
// .NET 8:没有内置的有序字典
// 需要自己实现或用第三方库

// .NET 9
var dict = new OrderedDictionary<string, int>();
dict.Add("first", 1);
dict.Add("second", 2);
dict.Add("third", 3);

// 保持插入顺序
foreach (var kvp in dict)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// 输出顺序:first, second, third

// 按索引访问
var item = dict[0];  // first: 1

12. PriorityQueue.Remove

复制代码
var queue = new PriorityQueue<string, int>();
queue.Enqueue("low", 10);
queue.Enqueue("high", 1);
queue.Enqueue("medium", 5);

// 移除元素
bool removed = queue.Remove("high", out string? element, out int priority);
// removed = true, element = "high", priority = 1

13. Guid v7(时间有序)

复制代码
// .NET 8:生成随机 GUID
var guid1 = Guid.NewGuid();  // 完全随机

// .NET 9:生成时间有序 GUID
var guid2 = Guid.CreateVersion7();
// 基于时间排序,适合作为数据库主键

// 从时间戳创建
var guid3 = Guid.CreateVersion7(DateTimeOffset.UtcNow);

优势:数据库索引友好、分布式系统中保持排序、兼容 v1/v4 格式

五、安全改进

14. BinaryFormatter 完全移除 ⚠️

这是 .NET 9 最重要的安全改进之一

复制代码
// .NET 8:已标记为 obsolete
// [Obsolete] BinaryFormatter

// .NET 9:完全移除,无法使用
// var formatter = new BinaryFormatter();  // 编译错误

为什么移除? BinaryFormatter 存在严重的反序列化漏洞,已被大量 CVE 利用

替代方案

场景 推荐方案
JSON 序列化 System.Text.Json
二进制序列化 protobuf-net
高性能序列化 MessagePack
.NET 内部序列化 ISerializable + 自定义实现

迁移示例

复制代码
// 旧代码
using var formatter = new BinaryFormatter();
using var stream = new FileStream("data.bin", FileMode.Open);
var data = formatter.Deserialize(stream);

// 新代码
using var stream = new FileStream("data.json", FileMode.Open);
var data = await JsonSerializer.DeserializeAsync<MyData>(stream);

15. CET(Control-flow Enforcement Technology)影子栈

  • .NET 9 在支持 CET 的硬件上自动启用影子栈保护

  • 防止 ROP(Return-Oriented Programming)攻击

  • 对应用透明,无需代码修改

16. CryptographicOperations 改进

复制代码
// 一次性哈希(更安全,避免复用)
byte[] hash = CryptographicOperations.HashData(
    HashAlgorithmName.SHA256, data
);

// 一次性验证
bool valid = CryptographicOperations.VerifyHash(
    HashAlgorithmName.SHA256,
    data,
    signature
);

六、其他重要 API

17. ServerSentEvents 编解码

复制代码
// 旧做法:手动构建 SSE 格式
await response.WriteAsync("data: hello\n\n");

// .NET 9:内置 SSE 支持
using var writer = new SseWriter(response.Body);
await writer.WriteAsync(new SseMessage
{
    EventType = "message",
    Data = "hello"
});

18. System.Text.Json 改进

复制代码
// 枚举值字符串(无额外分配)
var options = new JsonSerializerOptions
{
    Converters = { new JsonStringEnumConverter() }
};

// UnmappedMemberHandling(严格模式)
var strictOptions = new JsonSerializerOptions
{
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
};
// 遇到未映射的 JSON 属性时抛出异常

19. Tensor<T>(AI 计算基块)

复制代码
// 用于 AI/ML 计算的基础张量类型
var tensor = Tensor.Create<float>(new[] { 2, 3 });  // 2x3 矩阵
tensor[0, 0] = 1.0f;
tensor[0, 1] = 2.0f;

// 数学运算
var result = tensor + tensor;
var multiplied = tensor * 2.0f;

20. TimeSpan 从整数创建

复制代码
// .NET 8:只支持 double
var ts = TimeSpan.FromSeconds(30.0);

// .NET 9:支持 int,避免浮点精度问题
var ts2 = TimeSpan.FromSeconds(30);     // int 重载
var ts3 = TimeSpan.FromMilliseconds(500);
var ts4 = TimeSpan.FromTicks(10000);

21. StringSyntax 增强

复制代码
// .NET 9 扩展了 StringSyntax 属性
public class MyOptions
{
    [StringSyntax(StringSyntaxAttribute.Regex)]
    public string? Pattern { get; set; }

    [StringSyntax(StringSyntaxAttribute.Json)]
    public string? Json { get; set; }

    [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)]
    public string? DateFormat { get; set; }

    [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)]
    public string? TimeFormat { get; set; }
}

实战场景

CountBy + AggregateBy 组合

复制代码
// 电商统计:每个类别的订单数和总金额
var stats = orders
    .CountBy(o => o.Category)
    .ToDictionary(x => x.Key, x => x.Value);

var totals = orders
    .AggregateBy(
        o => o.Category,
        0m,
        (total, order) => total + order.Amount
    );

Task.WhenEach 实时处理

复制代码
// 并发下载多个文件,按完成顺序保存
var downloads = urls.Select(url => HttpClient.GetAsync(url));
await foreach (var task in Task.WhenEach(downloads))
{
    var response = await task;
    var fileName = ExtractFileName(response.RequestMessage.RequestUri);
    await SaveFileAsync(fileName, response.Content);
    Console.WriteLine($"已保存: {fileName}");
}

Lock 替代 lock 语句

复制代码
public class ThreadSafeCache<TKey, TValue>
{
    private readonly Lock _lock = new();
    private readonly Dictionary<TKey, TValue> _cache = new();

    public async Task<TValue> GetOrAddAsync(TKey key, Func<Task<TValue>> factory)
    {
        using (await _lock.EnterAsync())
        {
            if (_cache.TryGetValue(key, out var value))
                return value;

            value = await factory();
            _cache[key] = value;
            return value;
        }
    }
}

BinaryFormatter 迁移

复制代码
// 旧:BinaryFormatter + FileStream
// 新:System.Text.Json + FileStream
public static async Task SaveDataAsync<T>(string path, T data)
{
    var options = new JsonSerializerOptions { WriteIndented = true };
    await using var stream = File.Create(path);
    await JsonSerializer.SerializeAsync(stream, data, options);
}

public static async Task<T?> LoadDataAsync<T>(string path)
{
    await using var stream = File.OpenRead(path);
    return await JsonSerializer.DeserializeAsync<T>(stream);
}

迁移建议

从 GroupBy 迁移到 CountBy/AggregateBy

复制代码
// 旧代码
var counts = list.GroupBy(x => x.Key).ToDictionary(g => g.Key, g => g.Count());

// 新代码
var counts = list.CountBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
// 注意:CountBy 返回 IEnumerable<KeyValuePair>,如需 Dictionary 需 ToDictionary

BinaryFormatter 迁移检查清单

  1. 全局搜索 BinaryFormatterFormatterAssemblyStyle

  2. 替换为 System.Text.Jsonprotobuf-net

  3. 测试所有序列化/反序列化路径

  4. 验证数据兼容性

注意事项

API 注意点
CountBy 返回 IEnumerable<KeyValuePair>,不是 Dictionary
Task.WhenEach 需要 .NET 9+,不支持 netstandard
Lock 不能替代 Monitor 的所有用法(如 TryEnter)
Guid v7 需要系统时钟,不保证全局唯一
BinaryFormatter .NET 9 完全移除,必须迁移

一句话总结

.NET 9 标准库带来 20+ 实用 API:LINQ 更简洁、并发更灵活、安全更可靠、性能更出色。


官方文档