.NET 8 运行时中的新增功能

本文介绍了适用于 .NET 8 的 .NET 运行时中的新增功能。

性能改进

.NET 8 包括对代码生成和实时 (JIT) 编译的改进:

  • Arm64 性能改进
  • SIMD 改进
  • 对 AVX-512 ISA 扩展的支持(请参阅 Vector512 和 AVX-512
  • 云原生改进
  • JIT 吞吐量改进
  • 循环和常规优化
  • 优化了对标记为 ThreadStaticAttribute 的字段的访问
  • 连续寄存器分配。 Arm64 有两个用于表矢量查找的指令,这要求其元组操作数中的所有实体都存在于连续寄存器中。
  • JIT/NativeAOT 现在可以使用 SIMD 展开和自动矢量化某些内存操作(例如比较、复制和归零),前提是它可以在编译时确定大小。

此外,动态按配置优化 (PGO) 已得到改进,现在默认启用。 不再需要使用运行时配置选项来启用它。 动态 PGO 与分层编译配合运作,以根据层级 0 中实施的其他检测来进一步优化代码。

平均而言,动态 PGO 将性能提高了约 15%。 在大约 4600 个测试的基准套件中,23% 的人认为性能提高了 20% 或更多。

Codegen 结构提升

.NET 8 包含一个新的 codegen 物理提升优化传递,用于通用化 JIT 提升结构变量的能力。 这种优化(也称为聚合的标量替换)将结构变量的字段替换为 JIT 随后能够更准确地推理和优化的结构变量。

JIT 已支持此优化,但存在一些很大的限制,包括:

  • 仅支持具有四个或更少字段的结构。
  • 仅当每个字段是基元类型或用简单结构包装的基元类型时,才受支持。

物理升级消除了这些限制,从而修复了一些长期存在的 JIT 问题。

垃圾回收

.NET 8 添加了动态调整内存限制的功能。 这在需求时有时无的云服务方案中非常有用。 为了提高成本效益,服务应随着需求波动而对资源消耗进行纵向扩展和缩减。 当服务检测到需求下降时,它可以通过降低内存限制来纵向缩减资源消耗。 以前,此操作可能会失败,因为垃圾回收器 (GC) 不了解这种变更,可能会分配比新限制更多的内存。 通过此更改,可以调用 RefreshMemoryLimit() API 来使用新的内存限制更新 GC。

有一些限制需要注意:

  • 在 32 位平台上(例如 Windows x86 和 Linux ARM),.NET 无法建立新的堆硬限制(如果还没有)。
  • API 可能会返回指示刷新失败的非零状态代码。 如果纵向缩减过于激进,并且 GC 没有回旋余地,则可能会发生这种情况。 在这种情况下,请考虑调用 GC.Collect(2, GCCollectionMode.Aggressive) 以收缩当前内存使用量,然后重试。
  • 如果纵向扩展内存限制超出 GC 认为进程在启动期间可以处理的大小,则 RefreshMemoryLimit 调用将成功,但它使用的内存不能超过它所认为的限制。

下面的代码片段演示如何调用 API。

GC.RefreshMemoryLimit();

还可以刷新与内存限制相关的一些 GC 配置设置。 以下代码片段将堆硬限制设置为 100 兆字节 (MiB):

AppContext.SetData("GCHeapHardLimit", (ulong)100 * 1_024 * 1_024);
GC.RefreshMemoryLimit();

如果硬性限制无效,例如,在堆硬性限制百分比为负值以及硬性限制太低的情况下,API 可能会引发 InvalidOperationException 异常。 如果刷新将设置的堆硬性限制(由于新的 AppData 设置或容器内存限制更改所暗示)低于已提交的值,则可能会发生这种情况。

移动应用的全球化

iOS、tvOS 和 MacCatalyst 中的移动应用可以使用配备了较轻型 ICU 捆绑包的新增混合全球化模式。 在混合模式下,全球化数据一部分从 ICU 捆绑包拉取,一部分从对原生 API 的调用中拉取。 混合模式为移动端支持的所有区域设置提供服务。

对于移动平台上无法在固定全球化模式下运行,并且使用从 ICU 数据中剪裁的区域性的应用,混合模式是最合适的模式。 如果要加载较小的 ICU 数据文件,也可以使用它。 (icudt_hybrid.dat 文件比默认的 ICU 数据文件 icudt.dat 小 34.5%。)

若要使用混合全球化模式,请将 HybridGlobalization MSBuild 属性设置为 true:

<PropertyGroup>
  <HybridGlobalization>true</HybridGlobalization>
</PropertyGroup>

有一些限制需要注意:

  • 由于本机 API 的限制,并非所有全球化 API 都在混合模式下受支持。
  • 一些受支持的 API 具有不同的行为。

若要检查你的应用程序是否受影响,请参阅行为差异

源生成的 COM 互操作

.NET 8 包含一个新的源生成器,它支持与 COM 接口互操作。 可使用 GeneratedComInterfaceAttribute 将接口标记为源生成器的 COM 接口。 然后,源生成器将生成代码,支持从 C# 代码到非托管代码的调用。 它还会生成代码来支持从非托管代码到 C# 的调用。 此源生成器与 LibraryImportAttribute 集成,你可在 LibraryImport 特性化方法中将类型和 GeneratedComInterfaceAttribute 结合用作参数和返回类型。

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[GeneratedComInterface]
[Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")]
partial interface IComInterface
{
    void DoWork();
}

internal partial class MyNativeLib
{
    [LibraryImport(nameof(MyNativeLib))]
    public static partial void GetComInterface(out IComInterface comInterface);
}

源生成器还支持新的 GeneratedComClassAttribute 特性,通过它可将实现带有 GeneratedComInterfaceAttribute 特性的接口的类型传递给非托管代码。 源生成器将生成公开 COM 对象所需的代码,该对象会实现接口并将调用转发到托管实现。

具有 GeneratedComInterfaceAttribute 特性的接口上的方法支持与 LibraryImportAttribute 相同的所有类型,LibraryImportAttribute 现在支持 GeneratedComInterface 特性化类型和 GeneratedComClass 特性化类型。

如果 C# 代码仅使用 GeneratedComInterface 特性化接口来包装非托管代码中的 COM 对象,或者包装 C# 中的托管对象以向非托管代码公开,你可使用 Options 属性中的选项来自定义要生成的代码。 这些选项意味着不需要为已知不会使用的场景编写封送处理程序。

源生成器使用新的 StrategyBasedComWrappers 类型来创建和管理 COM 对象包装器和托管对象包装器。 这个新类型处理为 COM 互操作提供预期 .NET 用户体验,同时为高级用户提供自定义点的情况。 如果应用程序具有自己的机制来定义 COM 中的类型,或者你需要支持源生成的 COM 当前不支持的场景,请考虑使用新的 StrategyBasedComWrappers 类型来为你的场景添加缺少的功能,并为 COM 类型获得相同的 .NET 用户体验。

如果使用的是 Visual Studio,则可通过新的分析器和代码修补程序轻松地将现有 COM 互操作代码转换为使用源生成的互操作。 在每个具有 ComImportAttribute 的接口旁边,都有一个灯泡提供了一个选项用于转换为源生成的互操作。 该修补程序将接口更改为使用 GeneratedComInterfaceAttribute 特性。 在实现具有 GeneratedComInterfaceAttribute 接口的每个类旁边,都有一个灯泡提供了一个选项,用于将 GeneratedComClassAttribute 特性添加到类型中。 转换类型后,可移动 DllImport 方法来使用 LibraryImportAttribute

限制

COM 源生成器不支持单元关联,不支持使用 new 关键字 激活 COM CoClass,也不支持以下 API:

配置绑定源生成器

.NET 8 引入了一个源生成器,用于在 ASP.NET Core 中提供 AOT 和适合剪裁的配置。 该生成器是现有的基于反射的实现的替代方法。

源生成器探测 Configure(TOptions)BindGet 调用来从中检索类型信息。 在项目中启用生成器后,编译器将隐式选择生成的方法,而非预先存在的基于反射的框架实现。

无需更改源代码即可使用生成器。 AOT Web 应用中默认启用该生成器。 对于其他项目类型,源生成器默认关闭,但你可通过在项目文件中将 EnableConfigurationBindingGenerator 属性设置为 true 来选择使用它:

<PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

以下代码演示了调用绑定器的示例。

public class ConfigBindingSG
{
    static void RunIt(params string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        IConfigurationSection section = builder.Configuration.GetSection("MyOptions");

        // !! Configure call - to be replaced with source-gen'd implementation
        builder.Services.Configure<MyOptions>(section);

        // !! Get call - to be replaced with source-gen'd implementation
        MyOptions? options0 = section.Get<MyOptions>();

        // !! Bind call - to be replaced with source-gen'd implementation
        MyOptions options1 = new();
        section.Bind(options1);

        WebApplication app = builder.Build();
        app.MapGet("/", () => "Hello World!");
        app.Run();
    }

    public class MyOptions
    {
        public int A { get; set; }
        public string S { get; set; }
        public byte[] Data { get; set; }
        public Dictionary<string, string> Values { get; set; }
        public List<MyClass> Values2 { get; set; }
    }

    public class MyClass
    {
        public int SomethingElse { get; set; }
    }
}

Core .NET 库

本节包含下列子主题:

反射

.NET 5 中引入了函数指针,但当时未添加对反射的相应支持。 对函数指针使用 typeof 或反射时(例如分别使用 typeof(delegate*<void>())FieldInfo.FieldType),返回了 IntPtr。 从 .NET 8 开始,将改为返回 System.Type 对象。 此类型提供对函数指针元数据的访问,包括调用约定、返回类型和参数。

注意

函数指针实例(它是函数的物理地址)继续表示为 IntPtr。 仅反射类型已更改。

新功能目前仅在 CoreCLR 运行时和 MetadataLoadContext 中实现。

已将新的 API 添加到 System.Type(例如 IsFunctionPointer)以及 System.Reflection.PropertyInfoSystem.Reflection.FieldInfoSystem.Reflection.ParameterInfo。 以下代码演示如何使用一些新 API 进行反射。

using System;
using System.Reflection;

// Sample class that contains a function pointer field.
public unsafe class UClass
{
    public delegate* unmanaged[Cdecl, SuppressGCTransition]<in int, void> _fp;
}

internal class FunctionPointerReflection
{
    public static void RunIt()
    {
        FieldInfo? fieldInfo = typeof(UClass).GetField(nameof(UClass._fp));

        // Obtain the function pointer type from a field.
        Type? fpType = fieldInfo?.FieldType;

        // New methods to determine if a type is a function pointer.
        Console.WriteLine(
        $"IsFunctionPointer: {fpType?.IsFunctionPointer}");
        Console.WriteLine(
            $"IsUnmanagedFunctionPointer: {fpType?.IsUnmanagedFunctionPointer}");

        // New methods to obtain the return and parameter types.
        Console.WriteLine($"Return type: {fpType?.GetFunctionPointerReturnType()}");

        if (fpType is not null)
        {
            foreach (Type parameterType in fpType.GetFunctionPointerParameterTypes())
            {
                Console.WriteLine($"Parameter type: {parameterType}");
            }
        }

        // Access to custom modifiers and calling conventions requires a "modified type".
        Type? modifiedType = fieldInfo?.GetModifiedFieldType();

        // A modified type forwards most members to its underlying type.
        Type? normalType = modifiedType?.UnderlyingSystemType;

        if (modifiedType is not null)
        {
            // New method to obtain the calling conventions.
            foreach (Type callConv in modifiedType.GetFunctionPointerCallingConventions())
            {
                Console.WriteLine($"Calling convention: {callConv}");
            }
        }

        // New method to obtain the custom modifiers.
        Type[]? modifiers =
            modifiedType?.GetFunctionPointerParameterTypes()[0].GetRequiredCustomModifiers();

        if (modifiers is not null)
        {
            foreach (Type modreq in modifiers)
            {
                Console.WriteLine($"Required modifier for first parameter: {modreq}");
            }
        }
    }
}

前面的示例生成以下输出:

IsFunctionPointer: True
IsUnmanagedFunctionPointer: True
Return type: System.Void
Parameter type: System.Int32&
Calling convention: System.Runtime.CompilerServices.CallConvSuppressGCTransition
Calling convention: System.Runtime.CompilerServices.CallConvCdecl
Required modifier for first parameter: System.Runtime.InteropServices.InAttribute

序列化

对 .NET 8 中的 System.Text.Json 序列化和反序列化功能进行了多种改进。 例如,可以自定义对 JSON 有效负载中未包含的成员的处理

以下部分介绍了其他序列化改进:

有关一般 JSON 序列化的详细信息,请参阅 .NET 中的 JSON 序列化和反序列化

对其他类型的内置支持

序列化程序对以下附加类型提供内置支持。

  • HalfInt128UInt128 数值类型。

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Memory<T>ReadOnlyMemory<T> 值。 byte 值序列化为 Base64 字符串,其他类型序列化为 JSON 数组。

    JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1, 2, 3 }); // "AQID"
    JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }); // [1,2,3]
    

源生成器

.NET 8 包括对 System.Text.Json 源生成器的增强,旨在使本机 AOT 体验与基于反射的序列化程序不相上下。 例如:

  • 源生成器现在支持序列化具有 requiredinit 属性的类型。 这些属性均已在基于反射的序列化中得到支持。

  • 改进了源生成的代码的格式设置。

  • JsonSourceGenerationOptionsAttributeJsonSerializerOptions 的功能奇偶一致性。 有关详细信息,请参阅指定选项(源生成)

  • 其他诊断(如 SYSLIB1034SYSLIB1039)。

  • 不要包含已忽略或不可访问的属性的类型。

  • 支持在任意类型种类中嵌套 JsonSerializerContext 声明。

  • 支持在弱类型源生成场景中使用编译器生成的类型或无法形容的类型。 由于编译器生成的类型无法由源生成器显式指定,因此 System.Text.Json 现可在运行时执行最接近的上级解析。 此分辨率确定用于序列化值的最合适的超类型。

  • 新的转换器类型 JsonStringEnumConverter<TEnum>。 本机 AOT 不支持现有 JsonStringEnumConverter 类。 可按如下所示批注枚举类型:

    [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
    public enum MyEnum { Value1, Value2, Value3 }
    
    [JsonSerializable(typeof(MyEnum))]
    public partial class MyContext : JsonSerializerContext { }
    

    有关详细信息,请参阅 将枚举字段序列化为字符串

  • 使用新的 JsonConverter.Type 属性可以查找非泛型 JsonConverter 实例的类型:

    Dictionary<Type, JsonConverter> CreateDictionary(IEnumerable<JsonConverter> converters)
        => converters.Where(converter => converter.Type != null)
                     .ToDictionary(converter => converter.Type!);
    

    属性可为 null,因为它为 JsonConverterFactory 实例返回 null,为 JsonConverter<T> 实例返回 typeof(T)

链接源生成器

JsonSerializerOptions 类包括一个补充现有 TypeInfoResolver 属性的新的 TypeInfoResolverChain 属性。 这些属性用于对链接源生成器的协定自定义。 添加新属性意味着不必在一个调用站点上指定所有链接的组件,可以在事后添加它们。 TypeInfoResolverChain 还允许你对链进行自检或从中删除组件。 有关详细信息,请参阅合并源生成器

此外,JsonSerializerOptions.AddContext<TContext>() 现已过时。 它已被 TypeInfoResolverTypeInfoResolverChain 属性取代。 有关详细信息,请参阅 SYSLIB0049

接口层次结构

.NET 8 添加了对从接口层次结构序列化属性的支持。

以下代码演示了一个示例,其中对立即实现的接口及其基接口的属性进行了序列化。

public static void InterfaceHierarchies()
{
    IDerived value = new DerivedImplement { Base = 0, Derived = 1 };
    string json = JsonSerializer.Serialize(value);
    Console.WriteLine(json); // {"Derived":1,"Base":0}
}

public interface IBase
{
    public int Base { get; set; }
}

public interface IDerived : IBase
{
    public int Derived { get; set; }
}

public class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}

命名策略

JsonNamingPolicy 包含用于 snake_case(带下划线)和 kebab-case(带连字符)属性名称转换的新命名策略。 这些策略的使用与现有 JsonNamingPolicy.CamelCase 策略类似:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
JsonSerializer.Serialize(new { PropertyName = "value" }, options);
// { "property_name" : "value" }

有关详细信息,请参阅使用内置命名策略

只读属性

现在可以反序列化到只读字段或属性(即没有 set 访问器的字段或属性)。

若要选择此全局支持,请将新选项 PreferredObjectCreationHandling 设置为 JsonObjectCreationHandling.Populate。 如果考虑兼容性问题,还可通过将 [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] 特性放置在要填充其属性的特定类型上或单个属性上来更精细地启用该功能。

例如,请考虑以下代码,该代码反序列化为具有两个只读属性的 CustomerInfo 类型。

public static void ReadOnlyProperties()
{
    CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>("""
        { "Names":["John Doe"], "Company":{"Name":"Contoso"} }
        """)!;

    Console.WriteLine(JsonSerializer.Serialize(customer));
}

class CompanyInfo
{
    public required string Name { get; set; }
    public string? PhoneNumber { get; set; }
}

[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class CustomerInfo
{
    // Both of these properties are read-only.
    public List<string> Names { get; } = new();
    public CompanyInfo Company { get; } = new()
    {
        Name = "N/A",
        PhoneNumber = "N/A"
    };
}

在 .NET 8 之前,输入值被忽略,NamesCompany 属性保留其默认值。

{"Names":[],"Company":{"Name":"N/A","PhoneNumber":"N/A"}}

现在,输入值用于在反序列化期间填充只读属性。

{"Names":["John Doe"],"Company":{"Name":"Contoso","PhoneNumber":"N/A"}}

有关填充反序列化行为的详细信息,请参阅填充已初始化的属性

禁用基于反射的默认值

默认情况下,现在可以禁用基于反射的序列化程序。 此禁用对于避免甚至未使用的反射组件意外获取 root 权限是很有用的,尤其是在经过剪裁的和本机 AOT 应用中。 若要通过要求将 JsonSerializerOptions 参数传递给 JsonSerializer 序列化和反序列化方法来禁用默认的基于反射的序列化,请在项目文件中将 JsonSerializerIsReflectionEnabledByDefault MSBuild 属性设置为 false

使用新的 IsReflectionEnabledByDefault API 检查功能开关的值。 如果你是基于 System.Text.Json 构建的库作者,则可以依靠此属性来配置默认值,而不会意外获取反射组件的 root 权限。

有关详细信息,请参阅禁用反射默认值

新的 JsonNode API 方法

JsonNodeSystem.Text.Json.Nodes.JsonArray 类型包括以下新方法。

public partial class JsonNode
{
    // Creates a deep clone of the current node and all its descendants.
    public JsonNode DeepClone();

    // Returns true if the two nodes are equivalent JSON representations.
    public static bool DeepEquals(JsonNode? node1, JsonNode? node2);

    // Determines the JsonValueKind of the current node.
    public JsonValueKind GetValueKind(JsonSerializerOptions options = null);

    // If node is the value of a property in the parent
    // object, returns its name.
    // Throws InvalidOperationException otherwise.
    public string GetPropertyName();

    // If node is the element of a parent JsonArray,
    // returns its index.
    // Throws InvalidOperationException otherwise.
    public int GetElementIndex();

    // Replaces this instance with a new value,
    // updating the parent object/array accordingly.
    public void ReplaceWith<T>(T value);

    // Asynchronously parses a stream as UTF-8 encoded data
    // representing a single JSON value into a JsonNode.
    public static Task<JsonNode?> ParseAsync(
        Stream utf8Json,
        JsonNodeOptions? nodeOptions = null,
        JsonDocumentOptions documentOptions = default,
        CancellationToken cancellationToken = default);
}

public partial class JsonArray
{
    // Returns an IEnumerable<T> view of the current array.
    public IEnumerable<T> GetValues<T>();
}

非公共成员

可以使用 JsonIncludeAttributeJsonConstructorAttribute 特性注释将非公共成员加入到给定类型的序列化协定中。

public static void NonPublicMembers()
{
    string json = JsonSerializer.Serialize(new MyPoco(42));
    Console.WriteLine(json);
    // {"X":42}

    JsonSerializer.Deserialize<MyPoco>(json);
}

public class MyPoco
{
    [JsonConstructor]
    internal MyPoco(int x) => X = x;

    [JsonInclude]
    internal int X { get; }
}

有关详细信息,请参阅使用不可变类型和非公共成员和访问器

流式处理反序列化 API

.NET 8 包括新的 IAsyncEnumerable<T> 流式处理反序列化扩展方法,例如 GetFromJsonAsAsyncEnumerable。 也有类似的方法返回 Task<TResult>,例如 HttpClientJsonExtensions.GetFromJsonAsync。 新的扩展方法会调用流式处理 API 并返回 IAsyncEnumerable<T>

以下代码演示如何使用新的扩展方法。

public async static void StreamingDeserialization()
{
    const string RequestUri = "https://api.contoso.com/books";
    using var client = new HttpClient();
    IAsyncEnumerable<Book?> books = client.GetFromJsonAsAsyncEnumerable<Book>(RequestUri);

    await foreach (Book? book in books)
    {
        Console.WriteLine($"Read book '{book?.title}'");
    }
}

public record Book(int id, string title, string author, int publishedYear);

WithAddedModifier 扩展方法

通过新的 WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) 扩展方法,可以轻松地对任意 IJsonTypeInfoResolver 实例的序列化协定进行修改。

var options = new JsonSerializerOptions
{
    TypeInfoResolver = MyContext.Default
        .WithAddedModifier(static typeInfo =>
        {
            foreach (JsonPropertyInfo prop in typeInfo.Properties)
            {
                prop.Name = prop.Name.ToUpperInvariant();
            }
        })
};

新的 JsonContent.Create 重载

现在可以使用剪裁安全协定或源生成的协定创建 JsonContent 实例。 新方法包括:

var book = new Book(id: 42, "Title", "Author", publishedYear: 2023);
HttpContent content = JsonContent.Create(book, MyContext.Default.Book);

public record Book(int id, string title, string author, int publishedYear);

[JsonSerializable(typeof(Book))]
public partial class MyContext : JsonSerializerContext
{
}

冻结 JsonSerializerOptions 实例

通过以下新方法可以控制何时冻结 JsonSerializerOptions 实例:

  • JsonSerializerOptions.MakeReadOnly()

    此重载设计为剪裁安全,因此会在未使用解析程序配置选项实例的情况下引发异常。

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    如果将 true 传递给此重载,则会在缺少反射解析程序的情况下使用默认反射解析程序填充选项实例。 此方法被标记为 RequiresUnreferenceCode/RequiresDynamicCode,因此不适合本机 AOT 应用程序。

使用新的 IsReadOnly 属性可以检查选项实例是否已冻结。

时间抽象

新的 TimeProvider 类和 ITimer 接口添加了时间抽象功能,让你可以在测试方案中模拟时间。 此外,还可以使用时间抽象,通过 Task.DelayTask.WaitAsync 来模拟依赖于时间进度的 Task 操作。 时间抽象支持以下基本时间操作:

  • 检索本地和 UTC 时间
  • 获取用于测量性能的时间戳
  • 创建计时器

以下代码片段演示了一些使用情况示例。

// Get system time.
DateTimeOffset utcNow = TimeProvider.System.GetUtcNow();
DateTimeOffset localNow = TimeProvider.System.GetLocalNow();

TimerCallback callback = s => ((State)s!).Signal();

// Create a timer using the time provider.
ITimer timer = _timeProvider.CreateTimer(
    callback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

TimeSpan period = _timeProvider.GetElapsedTime(providerTimestamp1, providerTimestamp2);
// Create a time provider that works with a
// time zone that's different than the local time zone.
private class ZonedTimeProvider(TimeZoneInfo zoneInfo) : TimeProvider()
{
    private readonly TimeZoneInfo _zoneInfo = zoneInfo ?? TimeZoneInfo.Local;

    public override TimeZoneInfo LocalTimeZone => _zoneInfo;

    public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) =>
        new ZonedTimeProvider(zoneInfo);
}

UTF8 改进

如果要启用将类型的类似字符串的表示形式写出到目标范围,请在类型上实现新的 IUtf8SpanFormattable 接口。 此新接口与 ISpanFormattable 密切相关,但面向 UTF8 和 Span<byte>,而不是 UTF16 和 Span<char>

IUtf8SpanFormattable 已在所有基元类型(以及其他)上实现,无论是面向 stringSpan<char> 还是 Span<byte>,其共享逻辑完全一致。 它完全支持所有格式(包括新的“B”二进制说明符)和所有区域性。 这意味着现在可以从 ByteComplexCharDateOnlyDateTimeDateTimeOffsetDecimalDoubleGuidHalfIPAddressIPNetworkInt16Int32Int64Int128IntPtrNFloatSByteSingleRuneTimeOnlyTimeSpanUInt16UInt32UInt64UInt128UIntPtrVersion 直接格式化为 UTF8。

新的 Utf8.TryWrite 方法向现有 MemoryExtensions.TryWrite 方法(基于 UTF16)提供基于 UTF8 的对应方法。 可以使用内插字符串语法将复杂表达式直接格式化为 UTF8 字节范围,例如:

static bool FormatHexVersion(
    short major,
    short minor,
    short build,
    short revision,
    Span<byte> utf8Bytes,
    out int bytesWritten) =>
    Utf8.TryWrite(
        utf8Bytes,
        CultureInfo.InvariantCulture,
        $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}",
        out bytesWritten);

该实现可识别格式值上的 IUtf8SpanFormattable,并使用它们的实现将其 UTF8 表示形式直接写入目标范围。

该实现还利用新的 Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32) 方法,该方法及其 Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32) 对应方法支持编码和解码到目标范围。 如果范围不够长,无法保留生成的状态,则方法将返回 false,而不是引发异常。

处理随机性的方法

System.RandomSystem.Security.Cryptography.RandomNumberGenerator 类型引入了两种处理随机性的新方法。

GetItems<T>()

新的 System.Random.GetItemsSystem.Security.Cryptography.RandomNumberGenerator.GetItems 方法可用于从输入集中随机选择指定数量的项。 以下示例显示如何使用 System.Random.GetItems<T>()(在 Random.Shared 属性提供的实例上)将 31 项随机插入数组。 此示例可用于“Simon”游戏,在此游戏中,玩家必须记住一系列彩色按钮。

private static ReadOnlySpan<Button> s_allButtons = new[]
{
    Button.Red,
    Button.Green,
    Button.Blue,
    Button.Yellow,
};

// ...

Button[] thisRound = Random.Shared.GetItems(s_allButtons, 31);
// Rest of game goes here ...

Shuffle<T>()

新的 Random.ShuffleRandomNumberGenerator.Shuffle<T>(Span<T>) 方法可用于随机化范围的顺序。 这些方法对于减少机器学习中的训练偏差很有用(因此,第一件事并不总是训练,但最后一件事总是测试)。

YourType[] trainingData = LoadTrainingData();
Random.Shared.Shuffle(trainingData);

IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData);

DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData);
model = chain.Fit(split.TrainSet);

IDataView predictions = model.Transform(split.TestSet);
// ...

以性能为中心的类型

.NET 8 引入了几种旨在提高应用性能的新类型。

  • 新的 System.Collections.Frozen 命名空间包括集合类型 FrozenDictionary<TKey,TValue>FrozenSet<T>。 创建集合后,这些类型就不允许对键和值进行任何更改。 此要求可实现更快的读取操作(例如,TryGetValue())。 对于在首次使用时填充,然后在长期服务期间保留的集合,这些类型特别有用,例如:

    private static readonly FrozenDictionary<string, bool> s_configurationData =
        LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true);
    
    // ...
    if (s_configurationData.TryGetValue(key, out bool setting) && setting)
    {
        Process();
    }
    
  • MemoryExtensions.IndexOfAny 这样的方法会查找传入集合中的任何值的第一个匹配项。 新的 System.Buffers.SearchValues<T> 类型旨在传递给此类方法。 相应地,.NET 8 添加了像 MemoryExtensions.IndexOfAny 这样的新方法重载,例如接受新类型的实例。 创建 SearchValues<T> 的实例时,将在那时派生优化后续搜索所需的所有数据,这意味着工作是预先完成的。

  • 新的 System.Text.CompositeFormat 类型可用于优化编译时未知的格式字符串(例如,格式字符串是从资源文件加载的)。 前面会花费一些额外的时间来完成诸如分析字符串之类的工作,但这可以节省每次使用时完成的工作。

    private static readonly CompositeFormat s_rangeMessage =
        CompositeFormat.Parse(LoadRangeMessageResource());
    
    // ...
    static string GetMessage(int min, int max) =>
        string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max);
    
  • 新的 System.IO.Hashing.XxHash3System.IO.Hashing.XxHash128 类型可实现快速 XXH3 和 XXH128 哈希算法。

System.Numerics 和 System.Runtime.Intrinsics

本部分介绍对 System.NumericsSystem.Runtime.Intrinsics 命名空间的改进。

  • Vector256<T>Matrix3x2Matrix4x4 改进了 .NET 8 上的硬件加速。 例如,Vector256<T> 在可能的情况下已重新实现为内部 2x Vector128<T> 操作。 这使得可以在 Vector128.IsHardwareAccelerated == trueVector256.IsHardwareAccelerated == false 时部分加速某些功能,例如在 Arm64 上。
  • 硬件内部函数现在使用 ConstExpected 属性进行批注。 这可确保用户知道基础硬件何时需要常量,以及非常量值何时可能会意外损害性能。
  • Lerp(TSelf, TSelf, TSelf)Lerp API 已添加到 IFloatingPointIeee754<TSelf>,因此也添加到了 float (Single)、double (Double) 和 Half。 此 API 可在两个值之间高效且正确地执行线性内插。

Vector512 和 AVX-512

.NET Core 3.0 扩展了 SIMD 支持,以包括适用于 x86/x64 的平台特定硬件内部函数 API。 .NET 5 添加了对 Arm64 的支持,.NET 7 添加了跨平台硬件内部函数。 .NET 8 通过引入 Vector512<T> 和支持 Intel 高级矢量扩展 512 (AVX-512) 说明,进一步支持 SIMD。

具体而言,.NET 8 包括对 AVX-512 的以下关键功能的支持:

  • 512 位矢量运算
  • 其他 16 个 SIMD 寄存器
  • 适用于 128 位、256 位和 512 位向量的其他说明

如果你有支持该功能的硬件,则 Vector512.IsHardwareAccelerated 现在报告 true

.NET 8 还在 System.Runtime.Intrinsics.X86 命名空间下添加了几个平台特定类:

这些类遵循与其他指令集体系结构 (ISA) 相同的常规形状,因为它们为仅适用于 64 位进程的指令公开 IsSupported 属性和嵌套 Avx512F.X64 类。 此外,每个类都有一个嵌套 Avx512F.VL 类,用于公开相应指令集的 Avx512VL(向量长度)扩展。

即使未在代码中显式使用特定于 Vector512Avx512F 的指令,仍可能受益于新的 AVX-512 支持。 使用 Vector128<T>Vector256<T> 时,JIT 可以隐式地利用其他寄存器和指令。 在由 Span<T>ReadOnlySpan<T> 公开的大多数操作以及为基元类型公开的许多数学 API 中,基类库在内部使用这些硬件内部函数。

数据验证

System.ComponentModel.DataAnnotations 命名空间包括用于云原生服务中的验证场景的新数据验证特性。 虽然预先存在的 DataAnnotations 验证程序适用于典型的 UI 数据输入验证(例如窗体上的字段),但新特性旨在验证非用户输入数据,例如配置选项。 除了新特性之外,还向 RangeAttributeRequiredAttribute 类型添加了新属性。

新的 API 说明
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
指定边界是否包含在允许的范围内。
System.ComponentModel.DataAnnotations.LengthAttribute 指定字符串或集合的下界和上界。 例如,[Length(10, 20)] 要求集合中至少有 10 个元素,最多有 20 个元素。
System.ComponentModel.DataAnnotations.Base64StringAttribute 验证字符串是有效的 Base64 表示形式。
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
分别指定允许列表和拒绝列表。 例如,[AllowedValues("apple", "banana", "mango")]

指标

新的 API 允许你在创建 MeterInstrument 对象时将键值对标记附加到其中。 已发布指标度量的聚合器可以使用标记来区分聚合值。

var options = new MeterOptions("name")
{
    Version = "version",
    // Attach these tags to the created meter.
    Tags = new TagList()
    {
        { "MeterKey1", "MeterValue1" },
        { "MeterKey2", "MeterValue2" }
    }
};

Meter meter = meterFactory!.Create(options);

Counter<int> counterInstrument = meter.CreateCounter<int>(
    "counter", null, null, new TagList() { { "counterKey1", "counterValue1" } }
);
counterInstrument.Add(1);

新的 API 包括:

加密

.NET 8 添加了对 SHA-3 哈希基元的支持。 (目前,具有 OpenSSL 1.1.1 或更高版本和 Windows 11 Build 25324 或更高版本的 Linux 支持 SHA-3。)可在其中使用 SHA-2 的 API 现在提供对 SHA-3 的补充。 对于哈希,这包括 SHA3_256SHA3_384SHA3_512;对于 HMAC,这包括 HMACSHA3_256HMACSHA3_384HMACSHA3_512;对于其中可配置算法的哈希,这包括 HashAlgorithmName.SHA3_256HashAlgorithmName.SHA3_384HashAlgorithmName.SHA3_512;对于 RSA OAEP 加密,这包括 RSAEncryptionPadding.OaepSHA3_256RSAEncryptionPadding.OaepSHA3_384RSAEncryptionPadding.OaepSHA3_512

以下示例演示如何使用 API(包括 SHA3_256.IsSupported 属性)来确定平台是否支持 SHA-3。

// Hashing example
if (SHA3_256.IsSupported)
{
    byte[] hash = SHA3_256.HashData(dataToHash);
}
else
{
    // ...
}

// Signing example
if (SHA3_256.IsSupported)
{
     using ECDsa ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
     byte[] signature = ec.SignData(dataToBeSigned, HashAlgorithmName.SHA3_256);
}
else
{
    // ...
}

SHA-3 支持目前旨在支持加密基元。 更高级别的构造和协议最初并不完全支持 SHA-3。 这些协议包括 X.509 证书、SignedXml 和 COSE。

网络

HTTPS 代理支持

到目前为止,HttpClient 支持的所有代理类型都允许“中间人”查看客户端正在连接到哪个站点,即使对于 HTTPS URI 也是如此。 HttpClient 现在支持 HTTPS 代理,它会在客户端和代理之间创建加密通道,以便可以完全隐私地处理所有请求。

要启用 HTTPS 代理,请设置 all_proxy 环境变量,或使用 WebProxy 类以编程方式控制代理。

Unix:export all_proxy=https://x.x.x.x:3218 Windows:set all_proxy=https://x.x.x.x:3218

还可以使用 WebProxy 类以编程方式控制代理。

基于流的 ZipFile 方法

.NET 8 包含 ZipFile.CreateFromDirectory 的新重载,通过它可以收集目录中包含的所有文件并压缩这些文件,然后将生成的 zip 文件存储到提供的流中。 同样,通过新的 ZipFile.ExtractToDirectory 重载,可提供包含压缩文件的流,并将其内容提取到文件系统中。 下面是新的重载:

namespace System.IO.Compression;

public static partial class ZipFile
{
    public static void CreateFromDirectory(
        string sourceDirectoryName, Stream destination);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory,
    Encoding? entryNameEncoding);

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, bool overwriteFiles) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
}

当磁盘空间受限时,这些新的 API 非常有用,因为它们让你不必再将磁盘用作中间步骤。

扩展库

本节包含下列子主题:

键化 DI 服务

键化依赖项注入 (DI) 服务提供了一种使用键来注册和检索 DI 服务的方法。 通过使用键,可以限定如何注册和使用服务的范围。 以下是一些新的 API:

以下示例演示了如何使用键化 DI 服务。

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BigCacheConsumer>();
builder.Services.AddSingleton<SmallCacheConsumer>();
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
WebApplication app = builder.Build();
app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());
app.MapGet("/big-cache", ([FromKeyedServices("big")] ICache cache) => cache.Get("data"));
app.MapGet("/small-cache", (HttpContext httpContext) => httpContext.RequestServices.GetRequiredKeyedService<ICache>("small").Get("data"));
app.Run();

class BigCacheConsumer([FromKeyedServices("big")] ICache cache)
{
    public object? GetData() => cache.Get("data");
}

class SmallCacheConsumer(IServiceProvider serviceProvider)
{
    public object? GetData() => serviceProvider.GetRequiredKeyedService<ICache>("small").Get("data");
}

public interface ICache
{
    object Get(string key);
}

public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

有关详细信息,请参阅 dotnet/runtime#64427

托管生命周期服务

托管服务现在有了更多可在应用程序生命周期内执行的选项。 IHostedService 之前提供 StartAsyncStopAsync,现在 IHostedLifecycleService 提供以下其他方法:

这些方法分别在现有点之前和之后运行。

下面的示例演示如何使用新 API。

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

internal class HostedLifecycleServices
{
    public async static void RunIt()
    {
        IHostBuilder hostBuilder = new HostBuilder();
        hostBuilder.ConfigureServices(services =>
        {
            services.AddHostedService<MyService>();
        });

        using (IHost host = hostBuilder.Build())
        {
            await host.StartAsync();
        }
    }

    public class MyService : IHostedLifecycleService
    {
        public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
    }
}

有关详细信息,请参阅 dotnet/runtime#86511

选项验证

源生成器

为了减少启动开销并改进验证功能集,我们引入了一个实现验证逻辑的源代码生成器。 以下代码对示例模型和验证程序类进行了演示。

public class FirstModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P1 { get; set; } = string.Empty;

    [Microsoft.Extensions.Options.ValidateObjectMembers(
        typeof(SecondValidatorNoNamespace))]
    public SecondModelNoNamespace? P2 { get; set; }
}

public class SecondModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P4 { get; set; } = string.Empty;
}

[OptionsValidator]
public partial class FirstValidatorNoNamespace
    : IValidateOptions<FirstModelNoNamespace>
{
}

[OptionsValidator]
public partial class SecondValidatorNoNamespace
    : IValidateOptions<SecondModelNoNamespace>
{
}

如果你的应用使用依赖项注入,你可按以下示例代码所示注入验证。

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<FirstModelNoNamespace>(
    builder.Configuration.GetSection("some string"));

builder.Services.AddSingleton<
    IValidateOptions<FirstModelNoNamespace>, FirstValidatorNoNamespace>();
builder.Services.AddSingleton<
    IValidateOptions<SecondModelNoNamespace>, SecondValidatorNoNamespace>();

ValidateOptionsResultBuilder 类型

.NET 8 引入了 ValidateOptionsResultBuilder 类型以促进 ValidateOptionsResult 对象的创建。 重要的是,此生成器允许累积多个错误。 以前,创建实现 IValidateOptions<TOptions>.Validate(String, TOptions) 所需的 ValidateOptionsResult 对象很困难,有时会导致分层验证错误。 如果有多个错误,验证过程通常会在第一个错误出现时停止。

以下代码片段演示了 ValidateOptionsResultBuilder 的用法示例。

ValidateOptionsResultBuilder builder = new();
builder.AddError("Error: invalid operation code");
builder.AddResult(ValidateOptionsResult.Fail("Invalid request parameters"));
builder.AddError("Malformed link", "Url");

// Build ValidateOptionsResult object has accumulating multiple errors.
ValidateOptionsResult result = builder.Build();

// Reset the builder to allow using it in new validation operation.
builder.Clear();

LoggerMessageAttribute 构造函数

LoggerMessageAttribute 现在提供额外的构造函数重载。 以前,必须选择无参数构造函数或需要所有参数(事件 ID、日志级别和消息)的构造函数。 在使用更少的代码指定所需参数时,新的重载提供了更大的灵活性。 如果未提供事件 ID,系统会自动生成一个事件 ID。

public LoggerMessageAttribute(LogLevel level, string message);
public LoggerMessageAttribute(LogLevel level);
public LoggerMessageAttribute(string message);

扩展指标

IMeterFactory 接口

可以在依赖项注入 (DI) 容器中注册新的 IMeterFactory 接口,并使用此接口以隔离方式创建 Meter 对象。

使用默认计量工厂实现将 IMeterFactory 注册到 DI 容器:

// 'services' is the DI IServiceCollection.
services.AddMetrics();

然后,使用者可以获取计量工厂并用它来创建新的 Meter 对象。

IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

MeterOptions options = new MeterOptions("MeterName")
{
    Version = "version",
};

Meter meter = meterFactory.Create(options);

MetricCollector<T> 类

通过新的 MetricCollector<T> 类,可记录指标度量值以及时间戳。 此外,通过此类还能灵活地使用所选的时间提供程序来准确地生成时间戳。

const string CounterName = "MyCounter";
DateTimeOffset now = DateTimeOffset.Now;

var timeProvider = new FakeTimeProvider(now);
using var meter = new Meter(Guid.NewGuid().ToString());
Counter<long> counter = meter.CreateCounter<long>(CounterName);
using var collector = new MetricCollector<long>(counter, timeProvider);

Assert.IsNull(collector.LastMeasurement);

counter.Add(3);

// Verify the update was recorded.
Assert.AreEqual(counter, collector.Instrument);
Assert.IsNotNull(collector.LastMeasurement);

Assert.AreSame(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
Assert.AreEqual(3, collector.LastMeasurement.Value);
Assert.AreEqual(now, collector.LastMeasurement.Timestamp);

System.Numerics.Tensors.TensorPrimitives

更新后的 System.Numerics.Tensors NuGet 包涵盖新 TensorPrimitives 命名空间中的 API,这些 API 添加了对张量运算的支持。 张量基元优化了数据密集型工作负载,例如 AI 和机器学习的负载。

语义搜索和检索扩充生成 (RAG) 等 AI 工作负载可通过使用相关数据增强提示来扩展大语言模型(如 ChatGPT)的自然语言功能。 对于这些工作负载,对向量(如用于查找最相关数据来回答问题的余弦相似性)的运算至关重要。 System.Numerics.Tensors.TensorPrimitives 包提供了用于向量运算的 API,这意味着你无需采用外部依赖项或编写自己的实现。

此包可替换 System.Numerics.Tensors 包

有关详细信息,请参阅 .NET 8 RC 2 公告博客文章

另请参阅