.NET 8 執行階段的新功能

本文會描述 .NET 8 的 .NET 執行階段中有何新功能。

效能改善

.NET 8 包含程式碼產生和 Just-In Time (JIT) 編譯的改進:

  • Arm64 效能改善
  • SIMD 改善
  • 支援 AVX-512 ISA 擴充 (請參閱 Vector512 和 AVX-512)
  • 雲端原生改善
  • JIT 輸送量改善
  • 迴圈和一般最佳化
  • 標示 ThreadStaticAttribute 欄位的最佳化存取
  • 連續暫存器配置。 Arm64 有兩個資料表向量查閱的指令,這需要其 Tuple 運算元中的所有實體都存在於連續暫存器中。
  • 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 Interop

.NET 8 包含新的來源產生器,可支援與 COM 介面互通。 您可以使用 GeneratedComInterfaceAttribute 將介面標示為來源產生器的 COM 介面。 然後,來源產生器會產生程式碼,以啟用從 C# 程式碼呼叫非受控程式碼。 它也會產生程式碼,以啟用從非受控程式碼呼叫 C#。 此來源產生器會與 LibraryImportAttribute 整合,且您可以使用具有 GeneratedComInterfaceAttribute 的型別作為參數,以及在 LibraryImport-attributed 方法中傳回型別。

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 物件包裝函式和受控物件包裝函式。 這個新型別會處理為 COM Interop 提供預期的 .NET 使用者體驗,同時為進階使用者提供自訂點。 如果您的應用程式有自己的機制可從 COM 定義型別,或如果您需要支援來源產生 COM 目前不支援的案例,請考慮使用新 StrategyBasedComWrappers 型別來新增案例的遺漏功能,同時為您的 COM 型別取得相同的 .NET 用戶體驗。

如果您使用 Visual Studio,新的分析器和程式碼修正可讓您輕鬆地將現有的 COM Interop 程式碼轉換成使用來源產生的 Interop。 在具有 ComImportAttribute 的每個介面旁邊,燈泡會提供一個選項,以轉換成來源產生的 Interop。 修正會將介面變更為使用 GeneratedComInterfaceAttribute 屬性。 此外,在使用 GeneratedComInterfaceAttribute 實作介面的每個類別旁邊,燈泡都會提供選項,以將 GeneratedComClassAttribute 屬性新增至型別。 轉換型別之後,您就可以移動 DllImport 方法以使用 LibraryImportAttribute

限制

COM 來源產生器不支援 Apartment 親和性,使用 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"}}

如需填入還原序列化行為的詳細資訊,請參閱填入初始化的屬性

停用以反映為基礎的預設值

您現在可以預設使用以反映為基礎的序列化程式來停用。 這項停用有助於避免意外將甚至未使用的反映元件設為根,特別是在修剪和原生 AOT 應用程式中。 若要藉由要求將 JsonSerializerOptions 引數傳遞至 JsonSerializer 序列化和還原序列化方法,以停用預設以反映為基礎的序列化,請將專案檔中的 JsonSerializerIsReflectionEnabledByDefault MSBuild 屬性設定為 false

使用新的 IsReflectionEnabledByDefault API 來檢查功能參數的值。 如果您是以 System.Text.Json 為基礎建置的程式庫作者,則可以依賴屬性來設定您的預設值,而不會意外地將反映元件設為根。

如需詳細資訊,請參閱停用反映預設值

新的 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 方法會提供 UTF8 型對應項目給現有以 UTF16 為基礎的 MemoryExtensions.TryWrite 方法。 您可以使用差補字串語法,將複雜運算式直接格式化為 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) 相同的一般形狀,因為它們會公開 IsSupported 屬性和巢狀 Avx512F.X64 類別,以取得僅適用於 64 位元處理程序的指令。 此外,每個類別都有巢狀 Avx512F.VL 類別,其會公開對應指令集的 Avx512VL (向量長度) 擴充。

即使您未在程式碼中明確使用 Vector512 特定或 Avx512F 特定指令,您仍可能受益於新的 AVX-512 支援。 使用 Vector128<T>Vector256<T> 時,JIT 可以隱含地利用其他暫存器和指令。 基底類別庫會在由 Span<T>ReadOnlySpan<T> 公開的多數作業,以及在針對基本類型公開的數學 API 中,在內部使用這些硬體內建功能。

資料驗證

System.ComponentModel.DataAnnotations 命名空間包含新的資料驗證屬性,適用於雲端原生服務中的驗證案例。 雖然預先存在的 DataAnnotations 驗證程式適用於一般 UI 的資料輸入驗證,例如表單上的欄位,新屬性的設計目的則是驗證非使用者輸入的資料,例如組態選項。 除了新屬性 (attribute) 之外,RangeAttributeRequiredAttribute 型別也會新增新屬性 (property)。

新增 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 雜湊基本類型的支援。 (Linux 目前支援 SHA-3 搭配 OpenSSL 1.1.1 或更新版本,以及 Windows 11 組建 25324 或更新版本。)當 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 Proxy

到目前為止,所有支援 HttpClient 的 Proxy 類型都允許「中間人」查看用戶端連線至哪個網站,即便針對 HTTPS URI 也是如此。 HttpClient 現在支援 HTTPS Proxy,這會建立用戶端與 Proxy 之間的加密通道,讓所有要求都可以使用完整隱私權來處理。

若要啟用 HTTPS Proxy,請設定 all_proxy 環境變數,或使用 WebProxy 類別以程式設計方式來控制 Proxy。

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

您也可以使用 WebProxy 類別,以程序設計方式來控制 Proxy。

以資料流為基礎的 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) 提供使用金鑰註冊和擷取 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 現在提供額外的建構函式多載。 之前,您必須選擇無參數建構函式,或需要所有參數 (事件識別碼、記錄層級和訊息) 的建構函式。 新的多載提供更大的彈性,以更少的程式碼來指定必要的參數。 如果您並未提供事件識別碼,則系統會自動產生一個。

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

擴充計量

IMeterFactory 介面

您可以在相依性插入 (DI) 容器中註冊新的 IMeterFactory 介面,並使用它以隔離的方式建立 Meter 物件。

使用預設計量處理站實作向 DI 容器註冊 IMeterFactory

// '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,可新增對 tensor 作業的支援。 tensor 基本類型會最佳化資料密集型工作負載,例如 AI 和機器學習。

語意搜尋和檢索增強生成 (RAG) 等 AI 工作負載會藉由增強相關資料的提示,來擴充大型語言模型如 ChatGPT 的自然語言功能。 針對這些工作負載,重要的是向量上的作業如餘弦相似度,以找出最相關的資料來回答問題。 System.Numerics.Tensors.TensorPrimitives 套件提供向量作業的 API,這表示您不需要採用外部相依性或撰寫您自己的實作。

此套件會取代 System.Numerics.Tensors 套件

如需詳細資訊,請參閱隆重發表 .NET 8 RC 2 部落格文章

另請參閱