.NET 9 的新增功能

了解 .NET 9 中的新增功能,并查找指向进一步文档的链接。

.NET 9 是 .NET 8 的继任者,特别侧重于云原生应用和性能。 作为标准期限支持 (STS) 版本,它将在 18 个月内受到支持。 可从此处下载 .NET 9

对于 .NET 9 的新增功能,工程团队在 GitHub 讨论中发布了 .NET 9 预览版更新。 这是提出问题并提供有关该版本反馈的好地方。

本文针对 .NET 9 预览版 2 进行了更新。 以下部分将介绍 .NET 9 中核心 .NET 库的更新。

.NET 运行时

序列化

System.Text.Json 中,.NET 9 提供了用于序列化 JSON 的新选项和新的单一实例,可以更轻松地使用 Web 默认值进行序列化。

缩进选项

JsonSerializerOptions 包括新的属性,可支持自定义写入 JSON 的缩进字符和缩进大小。

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 2,
};

string json = JsonSerializer.Serialize(
    new { Value = 1 },
    options
    );
Console.WriteLine(json);
//{
//                "Value": 1
//}

默认 Web 选项

如果要使用 ASP.NET Core 用于 Web 应用的默认选项进行序列化,请使用新的 JsonSerializerOptions.Web 单一实例。

string webJson = JsonSerializer.Serialize(
    new { SomeValue = 42 },
    JsonSerializerOptions.Web // Defaults to camelCase naming policy.
    );
Console.WriteLine(webJson);
// {"someValue":42}

LINQ

引入了新的方法 CountByAggregateBy。 借助这些方法,可以按键聚合状态,而无需通过 GroupBy 分配中间分组。

借助 CountBy,可以快速计算每个键的频率。 以下示例将查找文本字符串中最常出现的字词。

string sourceText = """
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Sed non risus. Suspendisse lectus tortor, dignissim sit amet, 
    adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam.
""";

// Find the most frequent word in the text.
KeyValuePair<string, int> mostFrequentWord = sourceText
    .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(word => word.ToLowerInvariant())
    .CountBy(word => word)
    .MaxBy(pair => pair.Value);

Console.WriteLine(mostFrequentWord.Key); // amet

借助 AggregateBy,可以实现通用性更强的工作流。 以下示例演示了如何计算与给定密钥关联的分数。

(string id, int score)[] data =
    [
        ("0", 42),
        ("1", 5),
        ("2", 4),
        ("1", 10),
        ("0", 25),
    ];

var aggregatedData =
    data.AggregateBy(
        keySelector: entry => entry.id,
        seed: 0,
        (totalScore, curr) => totalScore + curr.score
        );

foreach (var item in aggregatedData)
{
    Console.WriteLine(item);
}
//(0, 67)
//(1, 15)
//(2, 4)

借助 Index<TSource>(IEnumerable<TSource>),可以快速提取可枚举项的隐式索引。 现在,可以编写代码(如以下代码片段)来自动为集合中的项编制索引。

IEnumerable<string> lines2 = File.ReadAllLines("output.txt");
foreach ((int index, string line) in lines2.Index())
{
    Console.WriteLine($"Line number: {index + 1}, Line: {line}");
}

集合

System.Collections.Generic 命名空间中的 PriorityQueue<TElement,TPriority> 集合类型包括一个新的 Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) 方法,可使用它更新队列中某个项的优先级。

PriorityQueue.Remove() 方法

.NET 6 引入了 PriorityQueue<TElement,TPriority> 集合,用于提供简单且快速的数组堆实现。 数组堆通常存在一个问题,即它们 不支持优先级更新,这导致它们被禁止用于 Dijkstra 算法的变体等算法中。

虽然无法在现有集合中实现高效的 $O(\log n)$ 优先级更新,但使用新的 PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) 方法可以模拟优先级更新(尽管是在 $O(n)$ 时):

public static void UpdatePriority<TElement, TPriority>(
    this PriorityQueue<TElement, TPriority> queue,
    TElement element,
    TPriority priority
    )
{
    // Scan the heap for entries matching the current element.
    queue.Remove(element, out _, out _);
    // Re-insert the entry with the new priority.
    queue.Enqueue(element, priority);
}

此方法将取消阻止希望在渐进性能并非阻碍因素的上下文中实现图形算法的用户。 (此类上下文包括教育和原型制作。)例如,下面是使用新 API 的 Dijkstra 算法 的玩具实现。

密码

对于加密,.NET 9 在 CryptographicOperations 类型上添加了一个新的一次性哈希方法。 它还添加了使用 KMAC 算法的新类。

CryptographicOperations.HashData() 方法

.NET 包括哈希函数和相关函数的多个静态“一次性”实现。 这些 API 包括 SHA256.HashDataHMACSHA256.HashData。 最好使用一次性 API,因为它们可以提供最佳性能并减少或消除分配。

如果开发人员想要提供一个 API,以支持由调用方定义要使用的哈希算法的哈希,这通常是通过接受 HashAlgorithmName 参数来完成的。 但将该模式与一次性 API 配合使用需要切换每个可能的 HashAlgorithmName,然后使用适当的方法。 要解决此问题,.NET 9 引入了 CryptographicOperations.HashData API。 使用此 API,可以基于输入生成一次性哈希或 HMAC,其中使用的算法将由 HashAlgorithmName 确定。

static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data)
{
    byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data);
    ProcessHash(hash);
}

KMAC 算法

.NET 9 提供了 NIST SP-800-185 中指定的 KMAC 算法。 KECCAK 消息身份验证代码 (KMAC) 是基于 KECCAK 的伪随机函数和键控哈希函数。

以下新类将使用 KMAC 算法。 使用实例累积数据以生成 MAC,或使用静态 HashData 方法对 单个输入 进行一次性处理。

KMAC 在具有 OpenSSL 3.0 或更高版本的 Linux 上,以及 Windows 11 内部版本 26016 或更高版本上可用。 可以使用静态 IsSupported 属性来确定平台是否支持所需的算法。

if (Kmac128.IsSupported)
{
    byte[] key = GetKmacKey();
    byte[] input = GetInputToMac();
    byte[] mac = Kmac128.HashData(key, input, outputLength: 32);
}
else
{
    // Handle scenario where KMAC isn't available.
}

反射

在 .NET Core 版本和 .NET 5-8 中,对于为动态创建的类型生成程序集和发出反射元数据的支持仅限于可运行的 AssemblyBuilder。 对于从 .NET Framework 迁移到 .NET 的客户,缺少对保存程序集的支持通常是一个阻碍因素。 .NET 9 向 AssemblyBuilder 添加了公共 API,可用于保存发出的程序集。

新的持久化 AssemblyBuilder 实现独立于运行时和平台。 要创建持久化 AssemblyBuilder 实例,请使用新的 AssemblyBuilder.DefinePersistedAssembly API。 现有的 AssemblyBuilder.DefineDynamicAssembly API 可接受程序集名称和可选的自定义属性。 要使用新 API,请传递核心程序集 System.Private.CoreLib,它用于引用基本运行时类型。 未提供适用于 AssemblyBuilderAccess 的选项。 目前,持久化 AssemblyBuilder 实现仅支持保存,而不支持运行。 创建持久化 AssemblyBuilder 的实例后,定义模块、类型、方法或枚举、写入 IL 和其他所有用法的后续步骤保持不变。 这意味着,可以按原样使用现有的 System.Reflection.Emit 代码来保存程序集。 以下代码展示了一个示例。

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(
        new AssemblyName("MyAssembly"),
        typeof(object).Assembly
        );
    TypeBuilder tb = ab.DefineDynamicModule("MyModule")
        .DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod(
        "SumMethod",
        MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]
        );
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

public void UseAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    Type type = assembly.GetType("MyType");
    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, [5, 10]));
}

性能

.NET 9 包括对 64 位 JIT 编译器的增强功能,旨在提高应用性能。 这些编译器增强功能包括:

Arm64 矢量化,它是运行时的另一项新功能。

循环优化

改进循环的代码生成是 .NET 9 的首要任务,64 位编译器新增了一个优化功能,称为归纳变量 (IV) 扩展

IV 是一个变量,其值随包含循环迭代而更改。 在以下 for 循环中,i 是 IV for (int i = 0; i < 10; i++)。 如果编译器可以分析 IV 的值在其循环迭代中的演变方法,则它可以为相关表达式生成性能更高的代码。

考虑以下循环遍历数组的示例:

static int Sum(int[] arr)
{
    int sum = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        sum += arr[i];
    }

    return sum;
}

索引变量 i 的大小为 4 个字节。 在汇编级别,64 位寄存器通常用于保存 x64 上的数组索引,在以前的 .NET 版本中,编译器生成的代码会将 i 零扩展为 8 字节以进行数组访问,但在其他地方会继续将 i 视为 4 字节整数。 但是,将 i 扩展到 8 个字节需要 x64 上的附加指令。 使用 IV 扩展,64 位 JIT 编译器现在会在整个循环中将 i 扩展到 8 个字节,并省略零扩展。 循环访问数组非常常见,并且删除此指令的好处会很快提现出来。

本机 AOT 的内联改进

.NET 对于 64 位 JIT 编译器内联方的目标之一是尽可能多地消除阻止方法被内联的限制。 .NET 9 支持在 Windows x64、Linux x64 和 Linux Arm64 上内联对线程本地静态的访问

对于 static 类成员,该成员的一个实例存在于该类的所有实例中,这些实例“共享”该成员。 如果 static 成员的值对于每个线程都是唯一的,则将该值设为线程本地可以提高性能,因为这样并发原语便无需从其包含的线程安全访问 static 成员。

以前,访问本机 AOT 编译的程序中的线程本地静态数据需要 64 位 JIT 编译器向运行时发出调用以获取线程本地存储的基地址。 现在,编译器可以内联这些调用,从而减少了访问这些数据的指令。

PGO 改进:类型检查和强制转换

.NET 8 默认启用了动态按配置优化 (PGO)。 NET 9 扩展了 64 位 JIT 编译器的 PGO 实现,以分析更多代码模式。 启用分层编译后,64 位 JIT 编译器已将检测插入到你的程序中以分析其行为。 当它通过优化重新编译时,编译器会利用它在运行时构建的配置文件来做出特定于程序当前运行的决策。 在 .NET 9 中,64 位 JIT 编译器使用 PGO 数据来提高类型检查的性能

确定对象的类型需要调用运行时,这会导致性能下降。 当需要检查对象的类型时,64 位 JIT 编译器为了正确性会发出此调用(编译器通常不能排除任何可能性,即使它们看起来不太可能)。 但是,如果 PGO 数据表明某个对象可能是特定类型,则 64 位 JIT 编译器现在会发出一条快速路径,廉价地检查该类型,并仅在必要时才退回到调用运行时的慢速路径

.NET 库中的 Arm64 矢量化

新的 EncodeToUtf8 实现利用了 64 位 JIT 编译器在 Arm64 上发出多寄存器加载/存储指令的能力。 这种行为允许程序用更少的指令处理更大的数据块。 跨不同领域的 NET 应用应会在支持这些功能的 Arm64 硬件上看到吞吐量的改进。 某些基准测试将执行时间减少了一半以上。

.NET SDK

单元测试

本节介绍 .NET 9 中单元测试的更新:并行运行测试和终端记录器测试输出。

并行运行测试

在 .NET 9 中,dotnet test 与 MSBuild 的集成更完整。 由于 MSBuild 支持并行生成,因此可以跨不同目标框架并行运行同一项目的测试。 默认情况下,MSBuild 将并行进程的数量限制为计算机上处​​理器的数量。 你还可使用 -maxcpucount 开关设置自己的限制。 如果要选择退出并行度,请将 TestTfmsInParallel MSBuild 属性设置为 false

终端记录器测试显示

MSBuild 终端记录器现在直接支持 dotnet test 的测试结果报告。 在测试运行时(显示正在运行的测试名称)和测试完成后(任何测试错误都会以更好的方式呈现),你都可以获得功能更齐全的测试报告

有关终端记录器的详细信息,请参阅 dotnet 生成选项

.NET 工具前滚行为

.NET 工具是依赖框架的应用,你可以全局或本地安装它们,然后使用 .NET SDK 和安装的 .NET 运行时运行。 这些工具(如所有 .NET 应用)面向 .NET 的特定主版本。 默认情况下,应用不会在 .NET 的较新版本上运行。 工具作者可以通过设置 RollForward MSBuild 属性,选择在 .NET 运行时的较新版本上运行其工具。 但是,并非所有工具都支持这种情况。

用户可通过 dotnet tool install 的新选项决定应如何运行 .NET 工具。 通过 dotnet tool install 安装工具或通过 dotnet tool run <toolname> 运行工具时,可以指定名为 --allow-roll-forward 的新标志。 此选项使用前滚模式 Major 配置工具。 如果匹配的 .NET 版本不可用,此模式允许该工具在 .NET 的较新主版本上运行。 此功能可帮助早期采用者使用 .NET 工具,而无需工具作者更改任何代码。

另请参阅