.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兆字节:

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-attributed 方法中将 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-attributed 类型和 GeneratedComClass-attributed 类型。

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

源生成器使用新的 StrategyBasedComWrappers 类型来创建和管理 COM 对象包装器和托管对象包装器。 此新类型负责管理并提供符合预期的 .NET 用户体验以支持 COM 互操作,同时为高级用户提供定制化功能。 如果应用程序有自己的机制用于从 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 应用中启用,当 PublishTrimmed 设置为 true(.NET 8+ 应用)时。 对于其他项目类型,源生成器默认处于关闭状态,但可以通过将 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; }
    }
}

核心 .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 序列化和反序列化功能进行了许多改进。 例如,可以 自定义 POCO中不存在的 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 属性序列化类型。 这两者已在基于反射的序列化中受支持。

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

  • JsonSourceGenerationOptionsAttribute 功能与 JsonSerializerOptions等效。 有关详细信息,请参阅 指定选项(源生成)

  • 其他诊断(如 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 实例返回 nullJsonConverter<T> 实例返回 typeof(T)

链源生成器

JsonSerializerOptions 类包括一个新的 TypeInfoResolverChain 属性,该属性是对现有 TypeInfoResolver 属性的补充。 这些属性用于在合约自定义中连接源生成器。 添加新属性意味着无需在一个调用点上指定所有链式组件,可以在之后再添加它们。 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 方法提供了与现有基于 UTF16 的 MemoryExtensions.TryWrite 方法相对应的基于 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();
    
    // ...
    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>,因此已添加到 floatSingle)、doubleDouble)和 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)相同的常规形状,即它们公开 IsSupported 属性和嵌套 Avx512F.X64 类,以获取仅适用于 64 位进程的指令。 此外,每个类都有一个嵌套 Avx512F.VL 类,该类公开相应指令集的 Avx512VL(矢量长度)扩展。

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

数据验证

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

新建 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 包包括在新 System.Numerics.Tensors.TensorPrimitives 类型中添加支持张量运算的 API。 张量基元优化数据密集型工作负载,例如 AI 和机器学习。

语义搜索和检索扩充生成(RAG)等 AI 工作负载通过增加相关数据的提示,扩展了大型语言模型(如 ChatGPT)的自然语言功能。 对于这些工作负荷,对向量的操作(例如 余弦相似性 以查找最相关的数据以回答问题)至关重要。 TensorPrimitives 类型提供用于矢量操作的 API。

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

本机 AOT 支持

发布为本机 AOT 的选项最初是在 .NET 7 中引入的。 使用本机 AOT 发布应用会创建一个完全独立的应用版本,该版本不需要运行时,所有内容都包含在一个文件中。 .NET 8 为本机 AOT 发布带来了以下改进:

  • macOS上添加了对 x64 和 Arm64 体系结构的支持。

  • Linux 上本机 AOT 应用的大小最多可缩小 50%。 下表显示了在 .NET 7 和 .NET 8 上使用包含整个 .NET 运行时的本机 AOT 发布的“Hello World”应用的大小:

    操作系统 .NET 7 .NET 8
    Linux x64(具有 -p:StripSymbols=true 3.76 MB 1.84 MB
    Windows x64 2.85 MB 1.77 MB
  • 允许指定优化首选项:大小或速度。 默认情况下,编译器选择生成快速代码,同时注意应用程序的大小。 但是,可以使用 <OptimizationPreference> MSBuild 属性专门针对一个或另一个进行优化。 有关详细信息,请参阅优化 AOT 部署

使用本机 AOT 面向类似 iOS 的平台

.NET 8 开始着手为类似 iOS 的平台提供原生 AOT 支持。 现在可以在以下平台上使用本机 AOT 生成和运行 .NET iOS 和 .NET MAUI 应用程序:

  • ios
  • iossimulator
  • maccatalyst
  • tvos
  • tvossimulator

初步测试显示,对于使用本机 AOT 而不是 Mono 的 .NET iOS 应用,磁盘上的应用大小减少了约 35%。 .NET MAUI iOS 应用的磁盘应用大小最多可减少 50%。 此外,启动时间也更快。 与 Mono 相比,.NET iOS 应用具有大约 28% 更快的启动时间,而 .NET MAUI iOS 应用具有大约 50% 更好的启动性能。 .NET 8 支持是实验性的,只是整个功能的第一步。 有关详细信息,请参阅 .NET MAUI 博客文章中的 .NET 8 性能提升

本机 AOT 支持作为用于应用部署的一个选择加入功能提供;Mono 仍然是应用开发和部署的默认运行时。 若要在 iOS 设备上生成和运行具有本机 AOT 的 .NET MAUI 应用程序,请使用 dotnet workload install maui 安装 .NET MAUI 工作负载并 dotnet new maui -n HelloMaui 创建应用。 然后,将 MSBuild 属性 PublishAot 设置为项目文件中 true

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

设置所需的属性并运行 dotnet publish 时,如以下示例所示,将使用 Native AOT 部署应用。

dotnet publish -f net8.0-ios -c Release -r ios-arm64  /t:Run

局限性

并非所有 iOS 功能都与本机 AOT 兼容。 同样,并非所有 iOS 中常用的库都与 NativeAOT 兼容。 除了本机 AOT 部署的现有限制外,以下列表还显示了面向类似 iOS 的平台时的其他一些限制:

  • 仅在应用部署 (dotnet publish) 期间启用本机 AOT。
  • 只有 Mono 支持托管代码调试。
  • 与 .NET MAUI 框架的兼容性受到限制。

针对 Android 应用的 AOT 编译

为了减小应用大小,面向 Android 的 .NET 和 .NET MAUI 应用在发布模式下构建时使用分析的预先 (AOT) 编译模式。 与常规 AOT 编译相比,分析的 AOT 编译所影响的方法更少。 .NET 8 引入了 <AndroidStripILAfterAOT> 属性,允许你选择加入 Android 应用的进一步 AOT 编译,以进一步减小应用大小。

<PropertyGroup>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>

默认情况下,将 AndroidStripILAfterAOT 设置为 true 会替代默认 AndroidEnableProfiledAot 设置,从而允许(几乎)所有经过 AOT 编译的方法被修剪。 还可通过将两个属性都显式设置为 true 来结合使用分析的 AOT 和 IL 条带化:

<PropertyGroup>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
</PropertyGroup>

跨平台构建的 Windows 应用

在非 Windows 平台上生成面向 Windows 的应用时,生成的可执行文件现在使用任何指定的 Win32 资源(例如应用程序图标、清单、版本信息)进行更新。

以前,应用程序必须构建在 Windows 上才能有此类资源。 在跨构建支持中修复这一差距是一个受欢迎的请求,因为它是影响基础结构复杂性和资源使用情况的重要难题。

另请参阅