.NET 6 史上最全攻略

歡迎使用.NET 6。今天的版本是.NET 團隊和社區一年多努力的結果。C# 10 和F# 6 提供了語言改進,使您的程式碼更簡單、更好。性能大幅提升,我們已經看到微軟降低了託管雲服務的成本。.NET 6 是第一個原生支援Apple Silicon (Arm64) 的版本,並且還針對Windows Arm64 進行了改進。我們構建了一個新的動態配置文件引導優化(PGO) 系統,該系統可提供僅在運行時才可能進行的深度優化。使用dotnet monitorOpenTelemetry改進了雲診斷。WebAssembly支援更有能力和性能。HTTP/3添加了新的API ,處理JSON數學和直接操作記憶體。.NET 6 將支援三年。開發人員已經開始將應用程式升級到.NET 6,我們在生產中聽到了很好的早期成果。.NET 6 已為您的應用程式做好準備。

您可以下載適用於Linux、macOS 和Windows 的.NET 6 。

請參閱ASP.NET CoreEntity FrameworkWindows Forms.NET MAUIYARPdotnet 監視器帖子,了解各種場景中的新增功能。

.NET 6 亮點

.NET 6 是:

該版本包括大約一萬次git 提交。即使這篇文章很長,它也跳過了許多改進。您必須下載並試用.NET 6 才能看到所有新功能。

支援

.NET 6 是一個長期支援(LTS) 版本,將支援三年。它支援多種作業系統,包括macOS Apple Silicon 和Windows Arm64。

Red Hat與.NET 團隊合作,在Red Hat Enterprise Linux 上支援.NET。在RHEL 8 及更高版本上,.NET 6 將可用於AMD 和Intel (x64_64)、ARM (aarch64) 以及IBM Z 和LinuxONE (s390x) 架構。

請開始將您的應用程式遷移到.NET 6,尤其是.NET 5 應用程式。我們從早期採用者那裡聽說,從.NET Core 3.1 和.NET 5 升級到.NET 6 很簡單。

Visual Studio 2022Visual Studio 2022 for Mac支援.NET 6 。Visual Studio 2019、Visual Studio for Mac 8 或MSBuild 16 不支援它。如果要使用.NET 6,則需要升級到Visual Studio 2022(現在也是64 位)。Visual Studio Code C# 擴展支援.NET 6 。

Azure App 服務:

注意:如果您的應用已經在應用服務上運行.NET 6 預覽版或RC 版本,則在將.NET 6 運行時和SDK 部署到您所在區域後,它將在第一次重新啟動時自動更新。如果您部署了一個獨立的應用程式,您將需要重新構建和重新部署。

統一擴展平台

.NET 6 為瀏覽器桌面物聯網移動應用程式提供了一個統一的平台。底層平台已更新,可滿足所有應用類型的需求,並便於在所有應用中重用程式碼。新功能和改進同時適用於所有應用程式,因此您在雲或移動設備上運行的程式碼的行為方式相同並具有相同的優勢。

.NET 開發人員的範圍隨著每個版本的發布而不斷擴大。機器學習WebAssembly是最近添加的兩個。例如,通過機器學習,您可以編寫在流數據中查找異常的應用程式。使用WebAssembly,您可以在瀏覽器中託管.NET 應用程式,就像HTML 和JavaScript 一樣,或者將它們與HTML 和JavaScript 混合使用

最令人興奮的新增功能之一是.NET Multi-platform App UI (.NET MAUI)。您現在可以在單個項目中編寫程式碼,從而跨桌面和移動作業系統提供現代客戶端應用程式體驗。.NET MAUI 將比.NET 6 稍晚發布。我們在.NET MAUI 上投入了大量時間和精力,很高興能夠發布它並看到.NET MAUI 應用程式投入生產。

當然,.NET 應用程式也可以在家中使用Windows 桌面(使用Windows FormsWPF)以及使用ASP.NET Core 在雲中。它們是我們提供時間最長的應用程式類型,並且仍然非常受歡迎,我們在.NET 6 中對其進行了改進。

面向 .NET 6

繼續以廣泛平台為主題,在所有這些作業系統上編寫.NET 程式碼很容易。

以 .NET 6 為目標,您需要使用.NET 6 目標框架,如下所示:

<TargetFramework>net6.0<TargetFramework>

net6.0 Target Framework Moniker (TFM) 使您可以訪問.NET 提供的所有跨平台API。如果您正在編寫控制台應用程式、ASP.NET Core 應用程式或可重用的跨平台庫,這是最佳選擇。

如果您針對特定作業系統(例如編寫Windows 窗體或iOS 應用程式),那麼還有另一組TFM(每個都針對不言而喻的作業系統)供您使用。它們使您可以訪問所有net6.0的API以及一堆特定於作業系統的API。

  • net6.0-android
  • net6.0-ios
  • net6.0-maccatalyst
  • net6.0-tvos
  • net6.0-windows

每個無版本TFM 都相當於針對.NET 6 支援的最低作業系統版本。如果您想要具體或訪問更新的API,可以指定作業系統版本。

net6.0和net6.0-windows TFMs 都支援(與.NET 5 相同)。Android 和Apple TFM 是.NET 6 的新功能,目前處於預覽階段。稍後的.NET 6 更新將支援它們。

作業系統特定的 TFM 之間沒有兼容性關係。 例如,net6.0-ios與 net6.0-tvos不兼容。 如果您想共享程式碼,您需要使用帶有#if 語句的源程式碼或帶有net6.0目標程式碼的二進位文件來實現。

性能

自從我們啟動.NET Core 項目以來,該團隊一直在不斷地關注性能。Stephen Toub在記錄每個版本的.NET 性能進展方面做得非常出色。歡迎查看.NET 6 中的性能改進的帖子。在這篇文章中,裡面包括您想了解的重大性能改進,包括文件IO、介面轉換、PGO 和System.Text.Json。

動態 PGO

動態輪廓引導優化(PGO)可以顯著提高穩態性能。例如,PGO 為TechEmpower JSON”MVC”套件的每秒請求數提高了26%(510K -&gt; 640K)。

動態PGO 建立在分層編譯的基礎上,它使方法能夠首先非常快速地編譯(稱為”第0 層”)以提高啟動性能,然後在啟用大量優化的情況下隨後重新編譯(稱為”第1 層”)一旦該方法被證明是有影響的。該模型使方法能夠在第0 層中進行檢測,以允許對程式碼的執行進行各種觀察。在第1 層重新調整這些方法時,從第0 層執行收集的資訊用於更好地優化第1 層程式碼。這就是機制的本質。

動態PGO 的啟動時間將比默認運行時稍慢,因為在第0 層方法中運行了額外的程式碼來觀察方法行為。

要啟用動態 PGO,請在應用程式將運行的環境中設置 DOTNET_TieredPGO=1。 您還必須確保啟用分層編譯(默認情況下)。 動態 PGO 是可選的,因為它是一種新的且有影響力的技術。 我們希望發布選擇加入使用和相關回饋,以確保它經過全面壓力測試。 我們對分層編譯做了同樣的事情。 至少一個非常大的 Microsoft 服務支援並已在生產中使用動態 PGO。 我們鼓勵您嘗試一下。

您可以在.NET 6中的性能帖子中看到更多關於動態PGO 優勢的資訊,包括以下微基準,它測量特定LINQ 枚舉器的成本。

private IEnumerator<long> _source = Enumerable.Range(0, long.MaxValue).GetEnumerator();

[Benchmark]
public void MoveNext() => _source.MoveNext();

這是有和沒有動態PGO 的結果。

方法 意思是 程式碼大小
PGO 已禁用 1.905 納秒 30乙
啟用PGO 0.7071 納秒 105乙

這是一個相當大的差異,但程式碼大小也有所增加,這可能會讓一些讀者感到驚訝。這是由JIT 生成的彙編程式碼的大小,而不是記憶體分配(這是一個更常見的焦點)。.NET 6 性能帖子對此有很好的解釋。

PGO 實現中常見的一種優化是”熱/冷分離”,其中經常執行的方法部分(「熱」)在方法開始時靠近在一起,而不經常執行的方法部分(「冷」)是移到方法的末尾。這樣可以更好地使用指令快取,並最大限度地減少可能未使用的程式碼負載。

作為上下文,介面調度是 .NET 中最昂貴的調用類型。 非虛擬方法調用是最快的,甚至更快的是可以通過內聯消除的調用。 在這種情況下,動態 PGO 為 MoveNext 提供了兩個(替代)調用站點。 第一個 – 熱的 – 是對 Enumerable+RangeIterator.MoveNext的直接調用,另一個 – 冷的 – 是通過 IEnumerator<int>的虛擬介面調用。 如果大多數時候最熱門的人都被叫到,那將是一個巨大的勝利。

這就是魔法。當 JIT 檢測此方法的第 0 層程式碼時,包括檢測此介面調度以跟蹤每次調用時 \_source的具體類型。 JIT 發現每次調用都在一個名為 Enumerable+RangeIterator的類型上,這是一個私有類,用於在 Enumerable實現內部實現 Enumerable.Range。因此,對於第 1 層,JIT 已發出檢查以查看 \_source的類型是否為 Enumerable+RangeIterator:如果不是,則跳轉到我們之前強調的執行正常介面調度的冷部分。但如果是 – 基於分析數據,預計絕大多數時間都是這種情況 – 然後它可以繼續直接調用非虛擬化的 Enumerable+RangeIterator.MoveNext方法。不僅如此,它還認為內聯 MoveNext 方法是有利可圖的。最終效果是生成的彙編程式碼有點大,但針對預期最常見的確切場景進行了優化。當我們開始構建動態 PGO 時,這些就是我們想要的那種勝利。

動態PGO 將在RyuJIT 部分再次討論。

文件 IO 改進

FileStream幾乎完全用.NET 6 重寫,重點是提高非同步文件IO 性能。在Windows 上,實現不再使用阻塞API,並且可以 快幾倍 !我們還改進了所有平台上的記憶體使用。在第一次非同步操作(通常分配)之後,我們已經使非同步操作 免分配 !此外,我們已經使Windows 和Unix 實現不同的邊緣情況的行為統一(這是可能的)。

這種重寫的性能改進使所有作業系統受益。對Windows 的好處是最大的,因為它遠遠落後。macOS 和Linux 用戶也應該會看到顯著FileStream的性能改進。

以下基準將100 MB 寫入新文件。

private byte[] _bytes = new byte[8_000];

[Benchmark]
public async Task Write100MBAsync()
{
    using FileStream fs = new("file.txt", FileMode.Create, FileAccess.Write, FileShare.None, 1, FileOptions.Asynchronous);
    for (int i = 0; i < 100_000_000 / 8_000; i++)
        await fs.WriteAsync(_bytes);
}

在帶有SSD 驅動器的Windows 上,我們觀察到 4倍的加速 和超過 1200倍的分配下降

方法 運行 意思是 比率 已分配
寫100MBAsync .NET 5.0 1,308.2 毫秒 1.00 3,809 KB
寫100MBAsync .NET 6.0 306.8 毫秒 0.24 3 KB

我們還認識到需要更高性能的文件 IO 功能:並發讀取和寫入,以及分散/收集 IO。 針對這些情況,我們為 System.IO.FileSystem.IO.RandomAccess類引入了新的 API。

async Task AllOrNothingAsync(string path, IReadOnlyList<ReadOnlyMemory<byte>> buffers)
{
    using SafeFileHandle handle = File.OpenHandle(
        path, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.Asynchronous,
        preallocationSize: buffers.Sum(buffer => buffer.Length)); // hint for the OS to pre-allocate disk space

    await RandomAccess.WriteAsync(handle, buffers, fileOffset: 0); // on Linux it's translated to a single sys-call!

該示例演示:

預分配大小功能提高了性能,因為寫入操作不需要擴展文件,並且文件不太可能被碎片化。這種方法提高了可靠性,因為寫入操作將不再因空間不足而失敗,因為空間已被保留。Scatter/Gather IO API 減少了寫入數據所需的系統調用次數。

更快的介面檢查和轉換

介面鑄造性能提高了16% – 38%。這種改進對於C# 與介面之間的模式匹配特別有用。

這張圖表展示了一個有代表性的基準測試的改進規模。

將.NET 運行時的一部分從C++ 遷移到託管C# 的最大優勢之一是它降低了貢獻的障礙。這包括介面轉換,它作為早期的.NET 6 更改移至C#。.NET 生態系統中懂C# 的人比懂C++ 的人多(而且運行時使用具有挑戰性的C++ 模式)。僅僅能夠閱讀構成運行時的一些程式碼是培養以各種形式做出貢獻的信心的重要一步。

歸功於 Ben Adams

System.Text.Json 源生成器

我們為System.Text.Json 添加了一個源程式碼生成器,它避免了在運行時進行反射和程式碼生成的需要,並且可以在構建時生成最佳序列化程式碼。序列化程式通常使用非常保守的技術編寫,因為它們必須如此。但是,如果您閱讀自己的序列化源程式碼(使用序列化程式),您可以看到明顯的選擇應該是什麼,可以使序列化程式在您的特定情況下更加優化。這正是這個新的源生成器所做的。

除了提高性能和減少記憶體之外,源程式碼生成器還生成最適合裝配修整的程式碼。這有助於製作更小的應用程式。

序列化POCO是一種非常常見的場景。使用新的源程式碼生成器,我們觀察到序列化速度比我們的基準 快1.6倍

方法 意思是 標準差 比率
串列器 243.1 納秒 9.54 納秒 1.00
SrcGenSerializer 149.3 納秒 1.91 納秒 0.62

TechEmpower快取基準測試平台或框架對來自資料庫的資訊進行記憶體快取。基準測試的.NET 實現執行快取數據的JSON 序列化,以便將其作為響應發送到測試工具。

請求** / ** 要求
net5.0 243,000 3,669,151
網6.0 260,928 3,939,804
net6.0 + JSON 源碼生成 364,224 5,499,468

我們觀察到約100K RPS 增益( 增加約40%)。與 MemoryCache 性能改進相結合時,.NET 6 的吞吐量比.NET 5 高50% !

C# 10

歡迎來到C# 10。C# 10 的一個主要主題是繼續從C# 9 中的頂級語句開始的簡化之旅。新功能從 Program.cs中刪除了更多的儀式,導致程式只有一行。 他們的靈感來自於與沒有 C# 經驗的人(學生、專業開發人員和其他人)交談,並了解什麼對他們來說最有效且最直觀。

大多數.NET SDK 模板都已更新,以提供現在可以使用C# 10 實現的更簡單、更簡潔的體驗。我們收到回饋說,有些人不喜歡新模板,因為它們不適合專家,刪除面向對象,刪除在編寫C# 的第一天學習的重要概念,或鼓勵在一個文件中編寫整個程式。客觀地說,這些觀點都不正確。新模型同樣適用於作為專業開發人員的學生。但是,它與.NET 6 之前的C 派生模型不同。

C# 10 中還有其他一些功能和改進,包括記錄結構。

全局使用指令

全局using 指令讓您using只需指定一次指令並將其應用於您編譯的每個文件。

以下示例顯示了語法的廣度:

  • global using System;
  • global using static System.Console;
  • global using Env = System.Environment;

您可以將global using語句放在任何 .cs 文件中,包括在 Program.cs中。

隱式 usings 是一個MSBuild 概念,它會根據SDK自動添加一組指令。例如,控制台應用程式隱式使用不同於ASP.NET Core。

隱式使用是可選的,並在a 中啟用PropertyGroup

  • <ImplicitUsings\&gt;enable\&lt;/ImplicitUsings>

隱式使用對於現有項目是可選的,但默認包含在新C# 項目中。有關詳細資訊,請參閱隱式使用

文件範圍的命名空間

文件範圍的命名空間使您能夠聲明整個文件的命名空間,而無需將剩餘內容嵌套在{ ...}中. 只允許一個,並且必須在聲明任何類型之前出現。

新語法是單個的一行:

namespaceMyNamespace;

classMyClass{...}// Not indented

這種新語法是三行縮進樣式的替代方案:

namespaceMyNamespace
{
classMyClass{...}// Everything is indented
}

好處是在整個文件位於同一個命名空間中的極其常見的情況下減少縮進。

記錄結構

C# 9 將記錄作為一種特殊的面向值的類形式引入。在C# 10 中,您還可以聲明結構記錄。C# 中的結構已經具有值相等,但記錄結構添加了==運算符和IEquatable<T>的實現,以及基於值的ToString實現:

public record structPerson
{
publicstringFirstName{get; init;}
publicstringLastName{get; init;}
}

就像記錄類一樣,記錄結構可以是”位置的”,這意味著它們有一個主構造函數,它隱式聲明與參數對應的公共成員:

public record structPerson(stringFirstName,stringLastName);

但是,與記錄類不同,隱式公共成員是_可變的自動實現的屬性_。這樣一來,記錄結構就成為了元組的自然成長故事。例如,如果您有一個返回類型(string FirstName, string LastName),並且您希望將其擴展為命名類型,您可以輕鬆地聲明相應的位置結構記錄並維護可變語義。

如果你想要一個具有隻讀屬性的不可變記錄,你可以聲明整個記錄結構readonly(就像你可以其他結構一樣):

publicreadonly record structPerson(stringFirstName,stringLastName);

C# 10 不僅支援記錄結構,還支援_所有_結構以及匿名類型的with表達式:

var updatedPerson = person with{FirstName=&quot;Mary&quot;};

F# 6

F# 6旨在讓F# 更簡單、更高效。這適用於語言設計、庫和工具。我們對F# 6(及更高版本)的目標是消除語言中讓用戶感到驚訝或阻礙學習F# 的極端情況。我們很高興能與F# 社區合作進行這項持續的努力。

讓 F# 更快、更互操作

新語法task {…}直接創建一個任務並啟動它。這是 F# 6 中最重要的功能之一,它使非同步任務更簡單、性能更高,並且與 C# 和其他 .NET 語言的互操作性更強。以前,創建 .NET 任務需要使用async {…}來創建任務並調用Async.StartImmediateAsTask

該功能task {…}建立在稱為「可恢復程式碼」RFC FS-1087的基礎之上。可恢復程式碼是一個核心特性,我們希望在未來使用它來構建其他高性能非同步和屈服狀態機。

F# 6 還為庫作者添加了其他性能特性,包括InlineIfLambda 和F#活動模式的未裝箱表示。一個特別顯著的性能改進在於列表和數組表達式的編譯,現在它們的速度提高了 4倍 ,並且調試也更好、更簡單。

讓 F# 更易學、更統一

F# 6 啟用expr[idx]索引語法。到目前為止,F# 一直使用 expr.[idx] 進行索引。刪除點符號是基於第一次使用 F# 用戶的反覆回饋,點的使用與他們期望的標準實踐有不必要的差異。在新程式碼中,我們建議系統地使用新的expr[idx]索引語法。作為一個社區,我們都應該切換到這種語法。

F# 社區為使 F# 語言在 F# 6 中更加統一做出了重要改進。其中最重要的是消除了 F# 縮進規則中的一些不一致和限制。使 F# 更加統一的其他設計添加包括添加as圖案;在計算表達式中允許「重載自定義操作」(對 DSL 有用);允許_丟棄use綁定並允許%B在輸出中進行二進位格式化。F# 核心庫添加了用於複製和更新列表、數組和序列的新函數,以及其他NativePtr內在函數。自 2.0 起棄用的 F# 的一些舊功能現在會導致錯誤。其中許多更改更好地使 F# 與您的期望保持一致,從而減少意外。

F# 6 還增加了對 F# 中其他「隱式」和「類型導向」轉換的支援。這意味著更少的顯式向上轉換,並為 .NET 樣式的隱式轉換添加了一流的支援。F# 也進行了調整,以更好地適應使用 64 位整數的數字型檔時代,並隱式擴展了 32 位整數。

改進 F# 工具

F# 6 中的工具改進使日常編碼更容易。新的”管道調試”允許您單步執行、設置斷點並檢查 F# 管道語法input |> f1 |> f2 的中間值。陰影值的調試顯示已得到改進,消除了調試時常見的混淆源。F# 工具現在也更高效,F# 編譯器並行執行解析階段。F# IDE 工具也得到了改進。F# 腳本現在更加健壯,允許您通過global.json文件固定使用的 .NET SDK 版本。

熱重載

Hot Reload 是另一個性能特性,專註於開發人員的生產力。它使您能夠對正在運行的應用程式進行各種程式碼編輯,從而縮短您等待應用程式重新構建、重新啟動或重新導航到您在進行程式碼更改後所在位置所需的時間。

Hot Reload 可通過dotnet watch CLI 工具和 Visual Studio 2022 使用。您可以將 Hot Reload 與多種應用類型一起使用,例如 ASP.NET Core、Blazor、.NET MAUI、控制台、Windows 窗體 (WinForms)、WPF、WinUI 3、Azure 函數等。

使用 CLI 時,只需使用 啟動您的 .NET 6 應用程式dotnet watch,進行任何受支援的編輯,然後在保存文件時(如在 Visual Studio Code 中),這些更改將立即應用。如果不支援更改,詳細資訊將記錄到命令窗口。

此影像顯示了一個使用dotnet watch. 我對.cs文件和.cshtml文件進行了編輯(如日誌中所述),兩者都應用於程式碼並在不到半秒的時間內非常快速地反映在瀏覽器中。

使用 Visual Studio 2022 時,只需啟動您的應用程式,進行支援的更改,然後使用新的”熱重載”按鈕(如下圖所示)應用這些更改。您還可以通過同一按鈕上的下拉菜單選擇在保存時應用更改。使用 Visual Studio 2022 時,熱重載可用於多個 .NET 版本,適用於 .NET 5+、.NET Core 和 .NET Framework。例如,您將能夠對按鈕的OnClickEvent處理程式進行程式碼隱藏更改。應用程式的Main方法不支援它。

注意:RuntimeInformation.FrameworkDescription中存在一個錯誤,該錯誤將在該影像中展示,很快就會修復。

Hot Reload 還與現有的 Edit and Continue 功能(在斷點處停止時)以及用於實時編輯應用程式 UI 的 XAML Hot Reload 協同工作。目前支援 C# 和 Visual Basic 應用程式(不是 F#)。

安全

.NET 6 中的安全性得到了顯著改進。它始終是團隊關注的重點,包括威脅建模、加密和深度防禦防禦。

在 Linux 上,我們依賴OpenSSL進行所有加密操作,包括 TLS(HTTPS 必需)。在 macOS 和 Windows 上,我們依賴作業系統提供的功能來實現相同的目的。對於每個新版本的 .NET,我們經常需要添加對新版本 OpenSSL 的支援。.NET 6 增加了對OpenSSL 3的支援。

OpenSSL 3 的最大變化是改進的FIPS 140-2模組和更簡單的許可。

.NET 6 需要 OpenSSL 1.1 或更高版本,並且會更喜歡它可以找到的最高安裝版本的 OpenSSL,直到並包括 v3。在一般情況下,當您使用的 Linux 發行版默認切換到 OpenSSL 3 時,您最有可能開始使用 OpenSSL 3。大多數發行版還沒有這樣做。例如,如果您在 Red Hat 8 或 Ubuntu 20.04 上安裝 .NET 6,您將不會(在撰寫本文時)開始使用 OpenSSL 3。

OpenSSL 3、Windows 10 21H1 和 Windows Server 2022 都支援ChaCha20Poly1305。您可以.NET 6 中使用這種新的經過身份驗證的加密方案(假設您的環境支援它)。

感謝 Kevin Jones對 ChaCha20Poly1305 的 Linux 支援。

我們還發布了新的運行時安全緩解路線圖。重要的是,您使用的運行時不受教科書攻擊類型的影響。我們正在滿足這一需求。在 .NET 6 中,我們構建了W^X英特爾控制流強制技術(CET)的初始實現。W^X 完全受支援,默認為 macOS Arm64 啟用,並且可以選擇加入其他環境。CET 是所有環境的選擇加入和預覽。我們希望在 .NET 7 中的所有環境中默認啟用這兩種技術。

Arm64

這些天來,對於筆記型電腦電腦、雲硬體和其他設備來說,Arm64 令人興奮不已。我們對 .NET 團隊感到同樣興奮,並正在盡最大努力跟上這一行業趨勢。我們直接與 Arm Holdings、Apple 和 Microsoft 的工程師合作,以確保我們的實施是正確和優化的,並且我們的計劃保持一致。這些密切的合作夥伴關係對我們幫助很大。

  • 特別感謝 Apple 在 M1 晶片發布之前向我們的團隊發送了一蒲式耳 Arm64 開發套件供我們使用,並提供了重要的技術支援。
  • 特別感謝 Arm Holdings,他們的工程師對我們的 Arm64 更改進行了程式碼審查,並進行了性能改進。

在此之前,我們通過 .NET Core 3.0 和 Arm32 添加了對 Arm64 的初始支援。該團隊在最近的幾個版本中都對 Arm64 進行了重大投資,並且在可預見的未來這將繼續下去。在 .NET 6 中,我們主要關注在 macOS 和 Windows Arm64 作業系統上支援新的 Apple Silicon 晶片和x64 模擬場景

您可以在 macOS 11+ 和 Windows 11+ Arm64 作業系統上安裝 Arm64 和 x64 版本的 .NET。我們必須做出多種設計選擇和產品更改以確保其奏效。

我們的策略是「親原生架構」。我們建議您始終使用與原生架構相匹配的 SDK,即 macOS 和 Windows Arm64 上的 Arm64 SDK。SDK 是大量的軟體。在 Arm64 晶片上本地運行的性能將比模擬高得多。我們更新了 CLI 以簡化操作。我們永遠不會專註於優化模擬 x64。

默認情況下,如果您dotnet run是帶有 Arm64 SDK 的 .NET 6 應用程式,它將作為 Arm64 運行。您可以使用參數輕鬆切換到以 x64 運行,例如-adotnet run -a x64. 相同的論點適用於其他 CLI 動詞。有關更多資訊,請參閱 適用於macOS 和Windows Arm64 的.NET 6 RC2 更新

我想確保涵蓋其中的一個微妙之處。當您使用-a x64時,SDK 仍以 Arm64 方式原生運行。.NET SDK 體系結構中存在進程邊界的固定點。在大多數情況下,一個進程必須全是 Arm64 或全是 x64。我正在簡化一點,但 .NET CLI 會等待 SDK 架構中的最後一個進程創建,然後將其作為您請求的晶片架構(如 x64)啟動。這就是您的程式碼運行的過程。這樣,作為開發人員,您可以獲得 Arm64 的好處,但您的程式碼可以在它需要的過程中運行。這僅在您需要將某些程式碼作為 x64 運行時才相關。如果你不這樣做,那麼你可以一直以 Arm64 的方式運行所有東西,這很棒。

Arm64支援

對於 macOS 和 Windows Arm64,以下是您需要了解的要點:

  • 支援並推薦 .NET 6 Arm64 和 x64 SDK。
  • 支援所有支援的 Arm64 和 x64 運行時。
  • .NET Core 3.1 和 .NET 5 SDK 可以工作,但提供的功能較少,並且在某些情況下不受完全支援。
  • dotnet test尚未與 x64 模擬一起正常工作。我們正在努力dotnet test將作為6.0.200 版本的一部分進行改進,並且可能更早。

有關更多完整資訊,請參閱.NET 對macOS 和Windows Arm64的支援。

此討論中缺少Linux。它不像macOS 和Windows 那樣支援x64 模擬。因此,這些新的CLI 特性和支援方法並不直接適用於Linux,Linux 也不需要它們。

視窗Arm64

我們有一個簡單的工具來演示.NET 運行的環境。

C:Usersrich>dotnet tool install -g dotnet-runtimeinfo
You can invoke the tool using the following command: dotnet-runtimeinfo
Tool 'dotnet-runtimeinfo' (version '1.0.5') was successfully installed.

C:Usersrich>dotnet runtimeinfo
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

**.NET information
Version: 6.0.0
FrameworkDescription: .NET 6.0.0-rtm.21522.10
Libraries version: 6.0.0-rtm.21522.10
Libraries hash: 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6

**Environment information
ProcessorCount: 8
OSArchitecture: Arm64
OSDescription: Microsoft Windows 10.0.22494
OSVersion: Microsoft Windows NT 10.0.22494.0

如您所見,該工具在Windows Arm64 上本機運行。我將向您展示ASP.NET Core 的樣子。

macOS Arm64

您可以看到在macOS Arm64 上的體驗是相似的,並且還展示了架構目標。

rich@MacBook-Air app % dotnet --version
6.0.100
rich@MacBook-Air app % dotnet --info | grep RID
 RID:         osx-arm64
rich@MacBook-Air app % cat Program.cs 
using System.Runtime.InteropServices;
using static System.Console;

WriteLine($"Hello, {RuntimeInformation.OSArchitecture} from {RuntimeInformation.FrameworkDescription}!");
rich@MacBook-Air app % dotnet run
Hello, Arm64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app % dotnet run -a x64
Hello, X64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app % 

這張圖片展示了Arm64 執行是Arm64 SDK 的默認設置,以及使用-a參數在目標Arm64 和x64 之間切換是多麼容易。完全相同的體驗適用於Windows Arm64。

此影像演示了相同的內容,但使用的是ASP.NET Core。我正在使用與您在上圖中看到的相同的.NET 6 Arm64 SDK。

Arm64 上的 Docker

Docker 支援在本機架構和模擬中運行的容器,本機架構是默認的。這看起來很明顯,但當大多數Docker Hub 目錄都是面向x64 時,這可能會讓人感到困惑。您可以使用-platform linux/amd64來請求x64 影像。

我們僅支援在Arm64 作業系統上運行Linux Arm64 .NET 容器映像。這是因為我們從不支援在QEMU中運行.NET ,這是Docker 用於架構模擬的。看來這可能是由於 QEMU 的限制

此影像演示了我們維護的控制台示例:mcr.microsoft.com/dotnet/samples。 這是一個有趣的示例,因為它包含一些基本邏輯,用於列印您可以使用的CPU 和記憶體限制資訊。我展示的影像設置了CPU 和記憶體限制。

自己試試吧:docker run --rm mcr.microsoft.com/dotnet/samples

Arm64 性能

Apple Silicon 和x64 模擬支援項目非常重要,但是,我們也普遍提高了Arm64 性能。

此影像演示了將堆棧幀的內容清零的改進,這是一種常見的操作。綠線是新行為,而橙色線是另一個(不太有益的)實驗,兩者都相對於基準線有所改善,由藍線表示。對於此測試,越低越好。

容器

.NET 6 更適合容器,主要基於本文中討論的所有改進,適用於Arm64 和x64。我們還進行了有助於各種場景的關鍵更改。使用.NET 6 驗證容器改進演示了其中一些改進正在一起測試。

Windows 容器改進和新環境變數也包含在11 月9 日(明天)發布的11 月.NET Framework 4.8 容器更新中。

發布說明可在我們的docker 存儲庫中找到:

Windows 容器

.NET 6 增加了對Windows 進程隔離容器的支援。如果您在 Azure Kubernetes 服務(AKS) 中使用Windows 容器,那麼您依賴於進程隔離的容器。進程隔離容器可以被認為與Linux 容器非常相似。Linux 容器使用cgroups,Windows 進程隔離容器使用Job Objects。Windows 還提供Hyper-V 容器,通過更強大的虛擬化提供更大的隔離。Hyper-V 容器的.NET 6 沒有任何變化。

此更改的主要價值是現在Environment.ProcessorCount將使用Windows 進程隔離容器報告正確的值。如果在64 核機器上創建2 核容器,Environment.ProcessorCount將返回2. 在以前的版本中,此屬性將報告機器上的處理器總數,與Docker CLI、Kubernetes 或其他容器編排器/運行時指定的限制無關。此值被.NET 的各個部分用於擴展目的,包括.NET 垃圾收集器(儘管它依賴於相關的較低級別的API)。社區庫也依賴此API 進行擴展。

我們最近在AKS 上使用大量pod 在生產中的Windows 容器上與客戶驗證了這一新功能。他們能夠以50% 的記憶體(與他們的典型配置相比)成功運行,這是以前導致異常的OutOfMemoryException水平StackOverflowException。他們沒有花時間找到最低記憶體配置,但我們猜測它明顯低於他們典型記憶體配置的50%。由於這一變化,他們將轉向更便宜的Azure 配置,從而節省資金。只需升級即可,這是一個不錯的、輕鬆的勝利。

優化縮放

我們從用戶那裡聽說,某些應用程式在Environment.ProcessorCount報告正確的值時無法實現最佳擴展。如果這聽起來與您剛剛閱讀的有關Windows 容器的內容相反,那麼它有點像。.NET 6 現在提供DOTNET_PROCESSOR_COUNT 環境變數來手動控制Environment.ProcessorCount的值。在典型的用例中,應用程式可能在64 核機器上配置為4核,並且在8或16核方面擴展得最好。此環境變數可用於啟用該縮放。

這個模型可能看起來很奇怪,其中Environment.ProcessorCount--cpus(通過Docker CLI)值可能不同。默認情況下,容器運行時面向核心等價物,而不是實際核心。這意味著,當你說你想要4 個核心時,你得到的CPU 時間與4 個核心相當,但你的應用程式可能(理論上)在更多的核心上運行,甚至在短時間內在64 核機器上運行所有64 個核心。這可能使您的應用程式能夠在超過4 個執行緒上更好地擴展(繼續示例),並且分配更多可能是有益的。這假定執行緒分配基於 Environment.ProcessorCount的值。如果您選擇設置更高的值,您的應用程式可能會使用更多記憶體。對於某些工作負載,這是一個簡單的權衡。至少,這是一個您可以測試的新選項。

Linux 和Windows 容器均支援此新功能。

Docker 還提供了一個CPU 組功能,您的應用程式可以關聯到特定的內核。在這種情況下不建議使用此功能,因為應用程式可以訪問的內核數量是具體定義的。我們還看到了將它與Hyper-V 容器一起使用時的一些問題,並且它並不是真正適用於那種隔離模式。

Debian 11 “bullseye”

我們密切關注Linux 發行版的生命周期和發布計劃,並嘗試代表您做出最佳選擇。Debian 是我們用於默認Linux 映像的Linux 發行版。如果您6.0從我們的一個容器存儲庫中提取標籤,您將提取一個Debian 映像(假設您使用的是Linux 容器)。對於每個新的.NET 版本,我們都會考慮是否應該採用新的Debian 版本。

作為一項政策,我們不會為了方便標籤而更改Debian 版本,例如6.0, mid-release。如果我們這樣做了,某些應用程式肯定會崩潰。這意味著,在發布開始時選擇Debian 版本非常重要。此外,這些影像得到了很多使用,主要是因為它們是”好標籤”的引用。

Debian 和.NET 版本自然不會一起計劃。當我們開始.NET 6 時,我們看到Debian “bullseye” 可能會在2021 年發布。我們決定從發布開始就押注於Bullseye我們開始使用.NET 6 Preview 1發布基於靶心的容器映像,並決定不再回頭。賭注是.NET 6 版本會輸掉與靶心版本的競爭。到8 月8 日,我們仍然不知道Bullseye 什麼時候發貨,距離我們自己的版本發布還有三個月,即11 月8 日。我們不想在預覽版Linux 上發布生產.NET 6,但我們堅持我們會輸掉這場競賽的計劃很晚。

當Debian 11 “bullseye”於8 月14 日發布時,我們感到非常驚喜。我們輸掉了比賽,但贏得了賭注。這意味著默認情況下,.NET 6 用戶從第一天開始就可以獲得最佳和最新的Debian。我們相信Debian 11 和.NET 6 將是許多用戶的絕佳組合。抱歉,剋星,我們中了靶心

較新的發行版在其軟體包提要中包含各種軟體包的較新主要版本,並且通常可以更快地獲得CVE 修復。這是對較新內核的補充。新發行版可以更好地為用戶服務。

再往前看,我們很快就會開始計劃對Ubuntu 22.04的支援。Ubuntu是另一個Debian 系列發行版,深受.NET 開發人員的歡迎。我們希望為新的Ubuntu LTS 版本提供當日支援。

Tianon Gravi 致敬,感謝他們為社區維護Debian 映像並在我們有問題時幫助我們。

Dotnet Monitor

dotnet monitor是容器的重要診斷工具。它作為 sidecar 容器鏡像已經有一段時間了,但處於不受支援的”實驗”狀態。作為.NET 6 的一部分,我們正在發布一個基於.NET 6 的dotnet monitor映像,該映像在生產中得到完全支援。

dotnet monitor已被Azure App Service 用作其ASP.NET Core Linux 診斷體驗的實現細節。這是預期的場景之一,建立在dotnet monitor 之上,以提供更高級別和更高價值的體驗。

您現在可以拉取新影像:

docker pull mcr.microsoft.com/dotnet/monitor:6.0

dotnet monitor使從.NET 進程訪問診斷資訊(日誌、跟蹤、進程轉儲)變得更加容易。在台式機上訪問所需的所有診斷資訊很容易,但是,這些熟悉的技術在使用容器的生產環境中可能不起作用。dotnet monitor提供了一種統一的方式來收集這些診斷工件,無論是在您的桌面電腦上還是在Kubernetes 集群中運行。收集這些診斷工件有兩種不同的機制:

  • 用於臨時收集工件的 HTTP API。當您已經知道您的應用程式遇到問題並且您有興趣收集更多資訊時,您可以調用這些API 端點。
  • 基於規則的配置 觸發器,用於始終在線收集工件。您可以配置規則以在滿足所需條件時收集診斷數據,例如,當您持續高CPU 時收集進程轉儲。

dotnet monitor為.NET 應用程式提供了一個通用的診斷API,可以使用任何工具在任何地方工作。「通用API」不是.NET API,而是您可以調用和查詢的Web API。dotnet monitor包括一個ASP.NET Web 伺服器,它直接與.NET 運行時中的診斷伺服器交互並公開來自診斷伺服器的數據設計dotnet monitor可實現生產中的高性能監控和安全使用,以控制對特權資訊的訪問。dotnet monitor通過非Internet 可定址的unix domain socket與運行時交互——跨越容器邊界。該模型通訊模型非常適合此用例。

結構化 JSON 日誌

JSON 格式化程式現在是aspnet.NET 6 容器映像中的默認控制台記錄器。.NET 5 中的默認設置為簡單的控制台格式化程式。進行此更改是為了使默認配置與依賴機器可讀格式(如JSON)的自動化工具一起使用。

影像的輸出現在如下所示aspnet

$ docker run --rm -it -p 8000:80 mcr.microsoft.com/dotnet/samples:aspnetapp
{"EventId":60,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository","Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","State":{"Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","path":"/root/.aspnet/DataProtection-Keys","{OriginalFormat}":"Storing keys in a directory u0027{path}u0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed."}}
{"EventId":35,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager","Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","State":{"Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","KeyId":"86cafacf-ab57-434a-b09c-66a929ae4fd7","{OriginalFormat}":"No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form."}}
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: //[::]:80","State":{"Message":"Now listening on: //[::]:80","address":"//[::]:80","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrlu002BC to shut down.","State":{"Message":"Application started. Press Ctrlu002BC to shut down.","{OriginalFormat}":"Application started. Press Ctrlu002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"Message":"Hosting environment: Production","envName":"Production","{OriginalFormat}":"Hosting environment: {envName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app","State":{"Message":"Content root path: /app","contentRoot":"/app","{OriginalFormat}":"Content root path: {contentRoot}"}}

Logging\_\_Console\_\_FormatterName可以通過設置或取消設置環境變數或通過程式碼更改來更改記錄器格式類型(有關更多詳細資訊,請參閱控制台日誌格式)。

更改後,您將看到如下輸出(就像.NET 5 一樣):

$ docker run --rm -it -p 8000:80 -e Logging__Console__FormatterName="" mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {8d4ddd1d-ccfc-4898-9fe1-3e7403bf23a0} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: //[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

注意:此更改不會影響開發人員電腦上的.NET SDK,例如dotnet run.此更改特定於aspnet容器映像。

支援 OpenTelemetry 指標

作為我們關注可觀察性的一部分,我們一直在為最後幾個.NET 版本添加對 OpenTelemetry 的支援。在.NET 6 中,我們添加了對OpenTelemetry Metrics API的支援。通過添加對OpenTelemetry 的支援,您的應用程式可以與其他OpenTelemetry系統無縫互操作。

System.Diagnostics.MetricsOpenTelemetry Metrics API 規範的.NET 實現。Metrics API 是專門為處理原始測量而設計的,目的是高效、同時地生成這些測量的連續摘要。

API 包括Meter可用於創建儀器對象的類。API 公開了四個工具類:CounterHistogramObservableCounter和,ObservableGauge以支援不同的度量方案。此外,API 公開MeterListener該類以允許收聽儀器記錄的測量值,以用於聚合和分組目的。

OpenTelemetry .NET 實現將被擴展以使用這些新的API,這些API 添加了對Metrics 可觀察性場景的支援。

圖書館測量記錄示例

     Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
    Counter<int> counter = meter.CreateCounter<int>("Requests");
    counter.Add(1);
    counter.Add(1, KeyValuePair.Create<string, object>("request", "read"));

聽力示例

  MeterListener listener = new MeterListener();
    listener.InstrumentPublished = (instrument, meterListener) =>
    {
        if (instrument.Name == "Requests" && instrument.Meter.Name == "io.opentelemetry.contrib.mongodb")
        {
            meterListener.EnableMeasurementEvents(instrument, null);
        }
    };
    listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
    {
        Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
    });
    listener.Start();

Windows Forms

我們繼續在 Windows 窗體中進行重要改進。.NET 6 包括更好的控制項可訪問性、設置應用程式範圍的默認字體、模板更新等的能力。

可訪問性改進

在此版本中,我們添加了用於CheckedListBoxLinkLabelPanelScrollBarTabControlTrackBarUIA 提供程式,它們使講述人等工具和測試自動化能夠與應用程式的元素進行交互。

默認字體

您現在可以使用.Application.SetDefaultFont

voidApplication.SetDefaultFont(Font font)

最小的應用程式

以下是帶有 .NET 6 的最小Windows 窗體應用程式

class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}

作為.NET 6 版本的一部分,我們一直在更新大多數模板,使其更加現代和簡約,包括Windows 窗體。我們決定讓Windows 窗體模板更傳統一些,部分原因是需要將[STAThread]屬性應用於應用程式入口點。然而,還有更多的戲劇而不是立即出現在眼前。

ApplicationConfiguration.Initialize()是一個源生成API,它在後台發出以下調用:

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetDefaultFont(newFont(...));
Application.SetHighDpiMode(HighDpiMode.SystemAware);

這些調用的參數可通過csproj 或props 文件中的MSBuild 屬性進行配置。

Visual Studio 2022 中的Windows 窗體設計器也知道這些屬性(目前它只讀取默認字體),並且可以向您顯示您的應用程式,就像它在運行時一樣:

模板更新

C# 的Windows 窗體模板已更新,以支援新的應用程式引導、global using指令、文件範圍的命名空間和可為空的引用類型。

更多運行時 designers

現在您可以構建通用設計器(例如,報表設計器),因為.NET 6 具有設計器和與設計器相關的基礎架構所缺少的所有部分。有關詳細資訊,請參閱此部落格文章

單文件應用

在.NET 6中,已為 Windows 和macOS 啟用記憶體中單文件應用程式。在.NET 5 中,這種部署類型僅限於 Linux。您現在可以為所有受支援的作業系統發布作為單個文件部署和啟動的單文件二進位文件。單文件應用不再將任何核心運行時程式集提取到臨時目錄。

這種擴展功能基於稱為”超級主機”的構建塊。”apphost” 是在非單文件情況下啟動應用程式的可執行文件,例如myapp.exe./myapp. Apphost 包含用於查找運行時、載入它並使用該運行時啟動您的應用程式的程式碼。Superhost 仍然執行其中一些任務,但使用所有CoreCLR 本機二進位文件的靜態鏈接副本。靜態鏈接是我們用來實現單一文件體驗的方法。本機依賴項(如NuGet 包附帶的)是單文件嵌入的顯著例外。默認情況下,它們不包含在單個文件中。例如,WPF 本機依賴項不是超級主機的一部分,因此會在單文件應用程式之外產生其他文件。您可以使用該設置IncludeNativeLibrariesForSelfExtract嵌入和提取本機依賴項。

靜態分析

我們改進了單文件分析器以允許自定義警告。如果您的API 在單文件發布中不起作用,您現在可以使用[RequiresAssemblyFiles]屬性對其進行標記,如果啟用了分析器,則會出現警告。添加該屬性還將使方法中與單個文件相關的所有警告靜音,因此您可以使用該警告將警告向上傳播到您的公共API。

PublishSingleFile 設置為true 時,會自動為exe 項目啟用單文件分析器,但您也可以通過將 EnableSingleFileAnalysis 設置為true 來為任何項目啟用它。 如果您想支援將庫作為單個文件應用程式的一部分,這將很有幫助。

在.NET 5 中,我們為單文件包中行為不同的Assembly.Location和一些其他API添加了警告。

壓縮

單文件包現在支援壓縮,可以通過將屬性設置EnableCompressionInSingleFiletrue. 在運行時,文件會根據需要解壓縮到記憶體中。壓縮可以為某些場景節省大量空間。

讓我們看一下與NuGet 包資源管理器一起使用的單個文件發布(帶壓縮和不帶壓縮)。

無壓縮: 172 MB

壓縮: 71.6 MB

壓縮會顯著增加應用程式的啟動時間,尤其是在Unix 平台上。Unix 平台有一個不能用於壓縮的無拷貝快速啟動路徑。您應該在啟用壓縮後測試您的應用程式,看看額外的啟動成本是否可以接受。

單文件調試

目前只能使用平台調試器(如WinDBG)來調試單文件應用程式。我們正在考慮使用更高版本的Visual Studio 2022 添加Visual Studio 調試。

macOS 上的單文件簽名

單文件應用程式現在滿足macOS 上的Apple 公證和簽名要求。具體更改與我們根據離散文件布局構建單文件應用程式的方式有關。

Apple 開始對macOS Catalina 實施新簽名和公證要求。我們一直在與Apple 密切合作,以了解需求,並尋找使.NET 等開發平台能夠在該環境中正常工作的解決方案。我們已經進行了產品更改並記錄了用戶工作流程,以滿足Apple 在最近幾個.NET 版本中的要求。剩下的差距之一是單文件簽名,這是在macOS 上分發.NET 應用程式的要求,包括在macOS 商店中。

IL 修整

該團隊一直致力於為多個版本進行IL 修整。.NET 6 代表了這一旅程向前邁出的重要一步。我們一直在努力使更激進的修剪模式安全且可預測,因此有信心將其設為默認模式。TrimMode=link以前是可選功能,現在是默認功能。

我們有一個三管齊下的修剪策略:

  • 提高平台的修剪能力。
  • 對平台進行注釋以提供更好的警告並使其他人也能這樣做。
  • 在此基礎上,讓默認的修剪模式更具侵略性,以便讓應用程式變小。

由於使用未注釋反射的應用程式的結果不可靠,修剪之前一直處於預覽狀態。有了修剪警告,體驗現在應該是可預測的。沒有修剪警告的應用程式應該正確修剪並且在運行時觀察到行為沒有變化。目前,只有核心的.NET 庫已經完全註解了修剪,但我們希望看到生態系統注釋修剪併兼容修剪

減小應用程式大小

讓我們使用SDK 工具之一的crossgen來看看這個修剪改進。它可以通過幾個修剪警告進行修剪,crossgen 團隊能夠解決。

首先,讓我們看一下將crossgen 發布為一個獨立的應用程式而無需修剪。它是80 MB(包括.NET 運行時和所有庫)。

然後我們可以嘗試(現在是舊版).NET 5 默認修剪模式,copyused. 結果降至55 MB。

新的.NET 6 默認修剪模式link將獨立文件大小進一步降低到36MB。

我們希望新的link修剪模式能更好地與修剪的期望保持一致:顯著節省和可預測的結果。

默認啟用警告

修剪警告告訴您修剪可能會刪除運行時使用的程式碼的地方。這些警告以前默認禁用,因為警告非常嘈雜,主要是由於 .NET 平台沒有參與修剪作為第一類場景。

我們對大部分 .NET 庫進行了注釋,以便它們產生準確的修剪警告。因此,我們覺得是時候默認啟用修剪警告了。ASP.NET Core 和 Windows 桌面運行時庫尚未注釋。我們計劃接下來注釋 ASP.NET 服務組件(在 .NET 6 之後)。我們希望看到社區在 .NET 6 發布後對 NuGet 庫進行注釋。

您可以通過設置<SuppressTrimAnalysisWarnings>true來禁用警告。

更多資訊:

與本機 AOT 共享

我們也為Native AOT實驗實現了相同的修剪警告,這應該會以幾乎相同的方式改善 Native AOT 編譯體驗。

數學

我們顯著改進了數學 API。社區中的一些人已經在享受這些改進

面向性能的 API

System.Math 中添加了面向性能的數學 API。如果底層硬體支援,它們的實現是硬體加速的。

新 API:

  • SinCos用於同時計算SinCos
  • ReciprocalEstimate用於計算 1 / x的近似值。
  • ReciprocalSqrtEstimate用於計算1 / Sqrt(x)的近似值。

新的重載:

  • Clamp, DivRem,MinMax支援nintnuint
  • AbsSign支援nint
  • DivRem 變體返回tuple

性能改進:

大整數性能

改進了從十進位和十六進位字元串中解析 BigIntegers。我們看到了高達89% 的改進,如下圖所示(越低越好)。

感謝約瑟夫·達席爾瓦

Complex API 現在注釋為 readonly

現在對各種API 進行了注釋,System.Numerics.Complexreadonly以確保不會對readonly值或傳遞的值進行複製in。

歸功於hrrrrustic 。

BitConverter 現在支援浮點到無符號整數位廣播

BitConverter 現在支援DoubleToUInt64Bits, HalfToUInt16Bits, SingleToUInt32Bits, UInt16BitsToHalf, UInt32BitsToSingle, 和UInt64BitsToDouble. 這應該使得在需要時更容易進行浮點位操作。

歸功於Michal Petryka 。

BitOperations 支援附加功能

BitOperations現在支援IsPow2,RoundUpToPowerOf2提供nint/nuint重載現有函數

感謝約翰凱利霍耀源羅賓林德納

Vector, Vector2, Vector3 和 Vector4 改進

Vector現在支援C# 9 中添加的原始類型nint和nuint原始類型。例如,此更改應該可以更簡單地使用帶有指針或平台相關長度類型的SIMD 指令。

Vector現在支援一種Sum方法來簡化計算向量中所有元素的「水平和」的需要。歸功於伊萬茲拉塔諾夫

Vector現在支援一種通用方法As<TFrom, TTo>來簡化在具體類型未知的通用上下文中處理向量。感謝霍耀源

重載支援Span已添加到Vector2Vector3Vector4以改善需要載入或存儲矢量類型時的體驗。

更好地解析標準數字格式

我們改進了標準數字類型的解析器,特別是.ToString.TryFormatParse。他們現在將理解對精度 >99 位小數的要求,並將為那麼多位數提供準確的結果。此外,解析器現在更好地支援方法中的尾隨零。

以下示例演示了之前和之後的行為。

  • 32.ToString(“C100”)->C132
    • .NET 6:

$32.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
– .NET 5:我們在格式化程式碼中人為限制只能處理 <= 99 的精度。對於精度 >= 100,我們改為將輸入解釋為自定義格式。
– 32.ToString(“H99”)-> 扔一個`FormatException`
– .NET 6:拋出 FormatException
– 這是正確的行為,但在這裡調用它是為了與下一個示例進行對比。
– 32.ToString(“H100”)->H132
– .NET 6:拋出FormatException
– .NET 5:H是無效的格式說明符。所以,我們應該拋出一個FormatException. 相反,我們將精度>= 100 解釋為自定義格式的錯誤行為意味著我們返回了錯誤的值。
– double.Parse(“9007199254740997.0”)->9007199254740998
– .NET 6 9007199254740996:。
– .NET 5:9007199254740997.0不能完全以IEEE 754 格式表示。使用我們當前的舍入方案,正確的返回值應該是9007199254740996. 但是,輸入的最後一部分迫使解析器錯誤地舍入結果並返回。.09007199254740998

System.Text.Json

System.Text.Json提供多種高性能API 用於處理JSON 文檔。在過去的幾個版本中,我們添加了新功能,以進一步提高JSON 處理性能並減輕對希望從NewtonSoft.Json遷移的人的阻礙。 此版本包括在該路徑上的繼續,並且在性能方面向前邁出了一大步,特別是在序列化程式源生成器方面。

JsonSerializer 源生成

注意:使用.NET 6 RC1 或更早版本的源程式碼生成的應用程式應重新編譯

幾乎所有.NET 序列化程式的支柱都是反射。反射對於某些場景來說是一種很好的能力,但不能作為高性能雲原生應用程式(通常(反)序列化和處理大量JSON 文檔)的基礎。反射是啟動、記憶體使用和程式集修整的問題。

運行時反射的替代方法是編譯時源程式碼生成。在.NET 6 中,我們包含一個新的源程式碼生成器作為 System.Text.Json. JSON 源程式碼生成器可以與多種方式結合使用JsonSerializer並且可以通過多種方式進行配置。

它可以提供以下好處:

  • 減少啟動時間
  • 提高序列化吞吐量
  • 減少私有記憶體使用
  • 刪除運行時使用System.ReflectionSystem.Reflection.Emit
  • IL 修整兼容性

默認情況下,JSON 源生成器為給定的可序列化類型發出序列化邏輯。JsonSerializer通過生成直接使用的源程式碼,這提供了比使用現有方法更高的性能Utf8JsonWriter。簡而言之,源程式碼生成器提供了一種在編譯時為您提供不同實現的方法,以使運行時體驗更好。

給定一個簡單的類型:

namespace Test
{
    internal class JsonMessage
    {
        public string Message { get; set; }
    }
}

源生成器可以配置為為示例JsonMessage類型的實例生成序列化邏輯。請注意,類名JsonContext是任意的。您可以為生成的源使用所需的任何類名。

using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(JsonMessage)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }
}

使用此模式的序列化程式調用可能類似於以下示例。此示例提供了可能的最佳性能。

using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);

JsonSerializer.Serialize(jsonMessage, JsonContext.Default.JsonMessage);
writer.Flush();

// Writer contains:
// {"Message":"Hello, world!"}

最快和最優化的源程式碼生成模式——基於Utf8JsonWriter——目前僅可用於序列化。Utf8JsonReader根據您的回饋,將來可能會提供對反序列化的類似支援。

源生成器還發出類型元數據初始化邏輯,這也有利於反序列化。JsonMessage要反序列化使用預生成類型元數據的實例,您可以執行以下操作:

JsonSerializer.Deserialize(json, JsonContext.Default.JsonMessage);

**JsonSerializer 支援 IAsyncEnumerable

您現在可以使用System.Text.Json(反)序列化IAsyncEnumerableJSON 數組。以下示例使用流作為任何非同步數據源的表示。源可以是本地電腦上的文件,也可以是資料庫查詢或Web 服務API 調用的結果。

JsonSerializer.SerializeAsync已更新以識別並為IAsyncEnumerable值提供特殊處理。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

static async IAsyncEnumerable<int> PrintNumbers(int n)
{
    for (int i = 0; i < n; i++) yield return i;
}

using Stream stream = Console.OpenStandardOutput();
var data = new { Data = PrintNumbers(3) };
await JsonSerializer.SerializeAsync(stream, data); // prints {"Data":[0,1,2]}

IAsyncEnumerable僅使用非同步序列化方法支援值。嘗試使用同步方法進行序列化將導致NotSupportedException被拋出。

流式反序列化需要一個新的 API 來返回IAsyncEnumerable<T>。我們為此添加了JsonSerializer.DeserializeAsyncEnumerable方法,您可以在以下示例中看到。

using System;
using System.IO;
using System.Text;
using System.Text.Json;

var stream = new MemoryStream(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
{
    Console.WriteLine(item);
}

此示例將按需反序列化元素,並且在使用特別大的數據流時非常有用。它僅支援從根級JSON 數組讀取,儘管將來可能會根據回饋放寬。

現有DeserializeAsync方法名義上支援IAsyncEnumerable<T>,但在其非流方法簽名的範圍內。它必須將最終結果作為單個值返回,如以下示例所示。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;

var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"{""Data"":[0,1,2,3,4]}"));
var result = await JsonSerializer.DeserializeAsync<MyPoco>(stream);
await foreach (int item in result.Data)
{
    Console.WriteLine(item);
}

public class MyPoco
{
    public IAsyncEnumerable<int> Data { get; set; }
}

在此示例中,反序列化器將IAsyncEnumerable在返回反序列化對象之前緩衝記憶體中的所有內容。這是因為反序列化器需要在返回結果之前消耗整個 JSON 值。

System.Text.Json:可寫 DOM 功能

可寫JSON DOM 特性System.Text.Json添加了一個新的簡單且高性能的編程模型。這個新的API 很有吸引力,因為它避免了需要強類型的序列化合約,並且與現有的JsonDocument類型相比,DOM 是可變的。

這個新的 API 有以下好處:

  • 在使用POCO類型是不可能或不希望的情況下,或者當JSON 模式不固定且必須檢查的情況下,序列化的輕量級替代方案。
  • 啟用對大樹子集的有效修改。例如,可以有效地導航到大型JSON 樹的子部分並從該子部分讀取數組或反序列化POCO。LINQ 也可以與它一起使用。

以下示例演示了新的編程模型。

    // Parse a JSON object
    JsonNode jNode = JsonNode.Parse("{"MyProperty":42}");
    int value = (int)jNode["MyProperty"];
    Debug.Assert(value == 42);
    // or
    value = jNode["MyProperty"].GetValue<int>();
    Debug.Assert(value == 42);

    // Parse a JSON array
    jNode = JsonNode.Parse("[10,11,12]");
    value = (int)jNode[1];
    Debug.Assert(value == 11);
    // or
    value = jNode[1].GetValue<int>();
    Debug.Assert(value == 11);

    // Create a new JsonObject using object initializers and array params
    var jObject = new JsonObject
    {
        ["MyChildObject"] = new JsonObject
        {
            ["MyProperty"] = "Hello",
            ["MyArray"] = new JsonArray(10, 11, 12)
        }
    };

    // Obtain the JSON from the new JsonObject
    string json = jObject.ToJsonString();
    Console.WriteLine(json); // {"MyChildObject":{"MyProperty":"Hello","MyArray":[10,11,12]}}

    // Indexers for property names and array elements are supported and can be chained
Debug.Assert(jObject["MyChildObject"]["MyArray"][1].GetValue<int>() == 11);

ReferenceHandler.IgnoreCycles

JsonSerializer(System.Text.Json)現在支援在序列化對象圖時忽略循環的能力。該ReferenceHandler.IgnoreCycles選項具有與Newtonsoft.Json ReferenceLoopHandling.Ignore類似的行為。一個關鍵區別是System.Text.Json 實現用null JSON 標記替換引用循環,而不是忽略對象引用。

您可以在以下示例中看到ReferenceHandler.IgnoreCycles的行為。在這種情況下,該Next屬性被序列化為null,因為否則它會創建一個循環。

class Node
{
    public string Description { get; set; }
    public object Next { get; set; }
}

void Test()
{
    var node = new Node { Description = "Node 1" };
    node.Next = node;

    var opts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };

    string json = JsonSerializer.Serialize(node, opts);
    Console.WriteLine(json); // Prints {"Description":"Node 1","Next":null}
}

源程式碼構建

通過源程式碼構建,您只需幾個命令即可在您自己的電腦上從源程式碼構建.NET SDK 。讓我解釋一下為什麼這個項目很重要。

源程式碼構建是一個場景,也是我們在發布.NET Core 1.0 之前一直與Red Hat 合作開發的基礎架構。幾年後,我們非常接近於交付它的全自動版本。對於Red Hat Enterprise Linux (RHEL) .NET 用戶來說,這個功能很重要。Red Hat 告訴我們,.NET 已經發展成為其生態系統的重要開發者平台。好的!

Linux 發行版的黃金標準是使用作為發行版存檔一部分的編譯器和工具鏈構建開源程式碼。這適用於.NET 運行時(用C++ 編寫),但不適用於任何用C# 編寫的程式碼。對於C# 程式碼,我們使用兩遍構建機制來滿足發行版要求。這有點複雜,但了解流程很重要。

Red Hat 使用.NET SDK (#1) 的Microsoft 二進位構建來構建.NET SDK 源程式碼,以生成SDK (#2) 的純開源二進位構建。之後,使用這個新版本的SDK (#2) 再次構建相同的SDK 源程式碼,以生成可證明的開源SDK (#3)。.NET SDK (#3) 的最終二進位版本隨後可供RHEL 用戶使用。之後,Red Hat 可以使用相同的SDK (#3) 來構建新的.NET 版本,而不再需要使用Microsoft SDK 來構建每月更新。

這個過程可能令人驚訝和困惑。開源發行版需要通過開源工具構建。此模式確保不需要Microsoft 構建的SDK,無論是有意還是無意。作為開發者平台,包含在發行版中的門檻比僅使用兼容許可證的門檻更高。源程式碼構建項目使.NET 能夠滿足該標準。

源程式碼構建的可交付成果是源程式碼壓縮包。源tarball 包含SDK 的所有源(對於給定版本)。從那裡,紅帽(或其他組織)可以構建自己的SDK 版本。Red Hat 政策要求使用內置源工具鏈來生成二進位tar 球,這就是他們使用兩遍方法的原因。但是源程式碼構建本身不需要這種兩遍方法。

在Linux 生態系統中,給定組件同時擁有源和二進位包或tarball 是很常見的。我們已經有了可用的二進位tarball,現在也有了源tarball。這使得.NET 與標準組件模式相匹配。

.NET 6 的重大改進是源tarball 現在是我們構建的產品。它過去需要大量的人工來製作,這也導致將源tarball 交付給Red Hat 的延遲很長。雙方都對此不滿意。

在這個項目上,我們與紅帽密切合作五年多。它的成功在很大程度上要歸功於我們有幸與之共事的優秀紅帽工程師的努力。其他發行版和組織已經並將從他們的努力中受益。

附帶說明一下,源程式碼構建是朝著可重現構建邁出的一大步,我們也堅信這一點。.NET SDK 和C# 編譯器具有重要的可重現構建功能。

庫 API

除了已經涵蓋的API 之外,還添加了以下API。

WebSocket 壓縮

壓縮對於通過網路傳輸的任何數據都很重要。WebSockets 現在啟用壓縮。我們使用了WebSockets 的擴展permessage-deflate實現,RFC 7692。它允許使用該DEFLATE演算法壓縮WebSockets 消息負載。此功能是GitHub 上Networking 的主要用戶請求之一。

與加密一起使用的壓縮可能會導致攻擊,例如CRIMEBREACH。這意味著不能在單個壓縮上下文中將秘密與用戶生成的數據一起發送,否則可以提取該秘密。為了讓用戶注意到這些影響並幫助他們權衡風險,我們將其中一個關鍵API 命名為DangerousDeflateOptions。我們還添加了關閉特定消息壓縮的功能,因此如果用戶想要發送秘密,他們可以在不壓縮的情況下安全地執行此操作。

禁用壓縮時WebSocket的記憶體佔用減少了約27%。

從客戶端啟用壓縮很容易,如下例所示。但是,請記住,伺服器可以協商設置,例如請求更小的窗口或完全拒絕壓縮。

var cws = new ClientWebSocket();
cws.Options.DangerousDeflateOptions = new WebSocketDeflateOptions()
{
    ClientMaxWindowBits = 10,
    ServerMaxWindowBits = 10
};

還添加了對 ASP.NET Core 的 WebSocket 壓縮支援。

歸功於伊萬茲拉塔諾夫

Socks 代理支援

SOCKS是一種代理伺服器實現,可以處理任何TCP 或UDP 流量,使其成為一個非常通用的系統。這是一個長期存在的社區請求,已添加到.NET 6中。

此更改增加了對Socks4、Socks4a 和Socks5 的支援。例如,它可以通過SSH 測試外部連接或連接到 Tor 網路

該類WebProxy現在接受socks方案,如以下示例所示。

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("socks5://127.0.0.1", 9050)
};
var httpClient = new HttpClient(handler);

歸功於Huo yaoyuan。

Microsoft.Extensions.Hosting — 配置主機選項 API

我們在IHostBuilder 上添加了一個新的ConfigureHostOptions API,以簡化應用程式設置(例如,配置關閉超時):

using HostBuilder host = new()
    .ConfigureHostOptions(o =>
    {
        o.ShutdownTimeout = TimeSpan.FromMinutes(10);
    })
    .Build();

host.Run();

在.NET 5 中,配置主機選項有點複雜:

using HostBuilder host = new()
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(o =>
        {
            o.ShutdownTimeout = TimeSpan.FromMinutes(10);
        });
    })
    .Build();

host.Run();

Microsoft.Extensions.DependencyInjection — CreateAsyncScope API

CreateAsyncScope創建API是為了處理服務的處置IAsyncDisposable。以前,您可能已經注意到處置IAsyncDisposable服務提供者可能會引發InvalidOperationException異常。

以下示例演示了新模式,CreateAsyncScope用於啟用using語句的安全使用。

await using (var scope = provider.CreateAsyncScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

以下示例演示了現有的問題案例:

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

await using var provider = new ServiceCollection()
        .AddScoped<Foo>()
        .BuildServiceProvider();

// This using can throw InvalidOperationException
using (var scope = provider.CreateScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

以下模式是先前建議的避免異常的解決方法。不再需要它。

var scope = provider.CreateScope();
var foo = scope.ServiceProvider.GetRequiredService<Foo>();
await ((IAsyncDisposable)scope).DisposeAsync();

感謝Martin Björkström 。

Microsoft.Extensions.Logging — 編譯時源生成器

.NET 6 引入了LoggerMessageAttribute類型。 此屬性是Microsoft.Extensions.Logging命名空間的一部分,使用時,它會源生成高性能日誌記錄API。源生成日誌支援旨在為現代.NET 應用程式提供高度可用和高性能的日誌解決方案。自動生成的源程式碼依賴於ILogger介面和LoggerMessage.Define功能。

LoggerMessageAttribute源生成器在用於partial日誌記錄方法時觸發。當被觸發時,它要麼能夠自動生成partial它正在裝飾的方法的實現,要麼生成編譯時診斷,並提供有關正確使用的提示。編譯時日誌記錄解決方案在運行時通常比現有的日誌記錄方法快得多。它通過最大限度地消除裝箱、臨時分配和副本來實現這一點。

與直接手動使用LoggerMessage.Define API相比,有以下好處:

  • 更短更簡單的語法:聲明性屬性使用而不是編碼樣板。
  • 引導式開發人員體驗:生成器發出警告以幫助開發人員做正確的事情。
  • 支援任意數量的日誌記錄參數。LoggerMessage.Define最多支援六個。
  • 支援動態日誌級別。這是LoggerMessage.Define單獨不可能的。

要使用LoggerMessageAttribute,消費類和方法需要是partial。程式碼生成器在編譯時觸發並生成partial方法的實現。

public static partial class Log
{
    [LoggerMessage(EventId = 0, Level = LogLevel.Critical, Message = "Could not open socket to `{hostName}`")]
    public static partial void CouldNotOpenSocket(ILogger logger, string hostName);
}

在前面的示例中,日誌記錄方法是static,並且在屬性定義中指定了日誌級別。在靜態上下文中使用屬性時,ILogger需要實例作為參數。您也可以選擇在非靜態上下文中使用該屬性。有關更多示例和使用場景,請訪問編譯時日誌記錄源生成器文檔。

System.Linq — 可枚舉的支援 Index 和 Range 參數

Enumerable.ElementAt方法現在接受來自可枚舉末尾的索引,如以下示例所示。

Enumerable.Range(1, 10).ElementAt(^2); // returns 9

添加了一個Enumerable.Take接受Range參數的重載。它簡化了對可枚舉序列的切片:

  • source.Take(..3)代替source.Take(3)
  • source.Take(3..)代替source.Skip(3)
  • source.Take(2..7)代替source.Take(7).Skip(2)
  • source.Take(^3..)代替source.TakeLast(3)
  • source.Take(..^3)代替source.SkipLast(3)
  • source.Take(7..3)而不是.source.TakeLast(7).SkipLast(3)

感謝@dixin 。

System.Linq — TryGetNonEnumeratedCount

TryGetNonEnumeratedCount方法嘗試在不強制枚舉的情況下獲取源可枚舉的計數。這種方法在枚舉之前預分配緩衝區很有用的場景中很有用,如下面的示例所示。

List<T> buffer = source.TryGetNonEnumeratedCount(out int count) ? new List<T>(capacity: count) : new List<T>();
foreach (T item in source)
{
    buffer.Add(item);
}

TryGetNonEnumeratedCount檢查實現ICollection/ ICollection<T>;或利用Linq 採用的一些內部優化的源

System.Linq — DistinctBy / UnionBy / IntersectBy / ExceptBy

新變體已添加到允許使用鍵選擇器函數指定相等性的集合操作中,如下例所示。

Enumerable.Range(1, 20).DistinctBy(x => x % 3); // {1, 2, 3}

var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) };
first.UnionBy(second, person => person.Age); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40), ("Drew", 33) }

System.Linq – MaxBy / MinBy

MaxByMinBy方法允許使用鍵選擇器查找最大或最小元素,如下例所示。

var people = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
people.MaxBy(person => person.Age); // ("Ashley", 40)

System.Linq — Chunk

Chunk可用於將可枚舉的源分塊為固定大小的切片,如下例所示。

IEnumerable<int[]> chunks = Enumerable.Range(0, 10).Chunk(size: 3); // { {0,1,2}, {3,4,5}, {6,7,8}, {9} }

歸功於羅伯特安德森

System.Linq—— // FirstOrDefault 採用默認參數的重載 LastOrDefaultSingleOrDefault

如果源可枚舉為空,則現有的FirstOrDefault /LastOrDefault /SingleOrDefault方法返回default(T)。添加了新的重載,它們接受在這種情況下返回的默認參數,如以下示例所示。

Enumerable.Empty\&lt;int\&gt;().SingleOrDefault(-1); // returns -1

感謝@ Foxtrek64 。

System.Linq — Zip 接受三個可枚舉的重載

Zip方法現在支援組合三個枚舉,如以下示例所示。

var xs = Enumerable.Range(1, 10);
var ys = xs.Select(x => x.ToString());
var zs = xs.Select(x => x % 2 == 0);

foreach ((int x, string y, bool z) in Enumerable.Zip(xs,ys,zs))
{
}

歸功於Huo yaoyuan。

優先隊列

PriorityQueue<TElement, TPriority>(System.Collections.Generic) 是一個新集合,可以添加具有值和優先順序的新項目。在出隊時,PriorityQueue 返回具有最低優先順序值的元素。您可以認為這個新集合類似於Queue<T>但每個入隊元素都有一個影響出隊行為的優先順序值。

以下示例演示了.PriorityQueue<string, int>

// creates a priority queue of strings with integer priorities
var pq = new PriorityQueue<string, int>();

// enqueue elements with associated priorities
pq.Enqueue("A", 3);
pq.Enqueue("B", 1);
pq.Enqueue("C", 2);
pq.Enqueue("D", 3);

pq.Dequeue(); // returns "B"
pq.Dequeue(); // returns "C"
pq.Dequeue(); // either "A" or "D", stability is not guaranteed.

歸功於Patryk Golebiowski

更快地將結構處理為字典值

CollectionsMarshal.GetValueRef是一個新的 不安全 API,它可以更快地更新字典中的結構值。新API 旨在用於高性能場景,而不是用於一般用途。它返回ref結構值,然後可以使用典型技術對其進行更新。

以下示例演示了如何使用新API:

ref MyStruct value = CollectionsMarshal.GetValueRef(dictionary, key);
// Returns Unsafe.NullRef<TValue>() if it doesn't exist; check using Unsafe.IsNullRef(ref value)
if (!Unsafe.IsNullRef(ref value))
{
    // Mutate in-place
    value.MyInt++;
}

在此更改之前,更新struct字典值對於高性能場景可能會很昂貴,需要字典查找和複製到堆棧的struct. 然後在更改之後struct,它將再次分配給字典鍵,從而導致另一個查找和複製操作。這種改進將密鑰散列減少到1(從2)並刪除了所有結構複製操作。

歸功於本亞當斯

新建 DateOnly 和 TimeOnly 結構

添加了僅限日期和時間的結構,具有以下特徵:

  • 每個都代表a 的一半DateTime,或者只是日期部分,或者只是時間部分。
  • DateOnly非常適合生日、周年紀念日和工作日。它與SQL Server 的date類型一致。
  • TimeOnly非常適合定期會議、鬧鐘和每周工作時間。它與SQL Server 的time類型一致。
  • 補充現有的日期/時間類型( DateTime, DateTimeOffset, TimeSpan, TimeZoneInfo)。
  • System命名空間中,在CoreLib 中提供,就像現有的相關類型一樣。

性能改進 DateTime.UtcNow

這種改進具有以下好處:

  • 修復了在Windows 上獲取系統時間的2.5 倍性能回歸。
  • 利用Windows 閏秒數據的5 分鐘滑動快取,而不是在每次調用時獲取。

在所有平台上支援 Windows 和 IANA 時區

這種改進具有以下好處:

  • 使用時的隱式轉換(//github.com/dotnet/runtime/pull/49412)TimeZoneInfo.FindSystemTimeZoneById
  • TimeZoneInfo通過: TryConvertIanaIdToWindowsIdTryConvertWindowsIdToIanaIdHasIanaId(//github.com/dotnet/runtime/issues/49407)上的新API 進行顯式轉換
  • 改進了使用不同時區類型的系統之間的跨平台支援和互操作。
  • 刪除需要使用TimeZoneConverter OSS 庫。該功能現在是內置的。

改進的時區顯示名稱

Unix 上的時區顯示名稱已得到改進

  • 消除由.返回的列表中的顯示名稱的歧義。TimeZoneInfo.GetSystemTimeZones
  • 利用ICU / CLDR 全球化數據。
  • 僅適用於Unix。Windows 仍然使用註冊表數據。這可能會在以後更改。

還進行了以下附加改進:

  • UTC 時區的顯示名稱和標準名稱被硬編碼為英語,現在使用與其餘時區數據相同的語言(CurrentUICulture在Unix 上,Windows 上的作業系統默認語言)。
  • 由於大小限制,Wasm 中的時區顯示名稱改為使用非本地化IANA ID。
  • TimeZoneInfo.AdjustmentRule嵌套類將其BaseUtcOffsetDelta內部屬性公開,並獲得一個新的構造函數,該構造函數baseUtcOffsetDelta作為參數。(//github.com/dotnet/runtime/issues/50256)
  • TimeZoneInfo.AdjustmentRule還獲得了在Unix 上載入時區的各種修復(//github.com/dotnet/runtime/pull/49733), (//github.com/dotnet/runtime/pull/50131)

改進了對 Windows ACL 的支援

System.Threading.AccessControl現在包括對與Windows 訪問控制列表(ACL) 交互的改進支援。新的重載被添加到MutexSemaphoreOpenExistingTryOpenExisting方法EventWaitHandle中。這些具有「安全許可權」實例的重載允許打開使用特殊Windows 安全屬性創建的執行緒同步對象的現有實例。

此更新與.NET Framework 中可用的API 匹配並且具有相同的行為。

以下示例演示了如何使用這些新API。

對於Mutex

var rights = MutexRights.FullControl;
string mutexName = "MyMutexName";

var security = new MutexSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
MutexAccessRule accessRule = new MutexAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdMutex, openedMutex1 and openedMutex2 point to the same mutex
Mutex createdMutex = MutexAcl.Create(initiallyOwned: true, mutexName, out bool createdNew, security);
Mutex openedMutex1 = MutexAcl.OpenExisting(mutexName, rights);
MutexAcl.TryOpenExisting(mutexName, rights, out Mutex openedMutex2);

為了Semaphore

var rights = SemaphoreRights.FullControl;
string semaphoreName = "MySemaphoreName";

var security = new SemaphoreSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
SemaphoreAccessRule accessRule = new SemaphoreAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdSemaphore, openedSemaphore1 and openedSemaphore2 point to the same semaphore
Semaphore createdSemaphore = SemaphoreAcl.Create(initialCount: 1,  maximumCount: 3, semaphoreName, out bool createdNew, security);
Semaphore openedSemaphore1 = SemaphoreAcl.OpenExisting(semaphoreName, rights);
SemaphoreAcl.TryOpenExisting(semaphoreName, rights, out Semaphore openedSemaphore2);

為了EventWaitHandle

var rights = EventWaitHandleRights.FullControl;
string eventWaitHandleName = "MyEventWaitHandleName";

var security = new EventWaitHandleSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
EventWaitHandleAccessRule accessRule = new EventWaitHandleAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdHandle, openedHandle1 and openedHandle2 point to the same event wait handle
EventWaitHandle createdHandle = EventWaitHandleAcl.Create(initialState: true, EventResetMode.AutoReset, eventWaitHandleName, out bool createdNew, security);
EventWaitHandle openedHandle1 = EventWaitHandleAcl.OpenExisting(eventWaitHandleName, rights);
EventWaitHandleAcl.TryOpenExisting(eventWaitHandleName, rights, out EventWaitHandle openedHandle2);

HMAC 一次性方法

System.Security.CryptographyHMAC類現在具有允許一次性計算HMAC而無需分配的靜態方法。這些添加類似於在先前版本中添加的用於哈希生成的一次性方法。

DependentHandle 現已公開

DependentHandle類型現在是公共的,具有以下 API 表面

namespace System.Runtime
{
    public struct DependentHandle : IDisposable
    {
        public DependentHandle(object? target, object? dependent);
        public bool IsAllocated { get; }
        public object? Target { get; set; }
        public object? Dependent { get; set; }
        public (object? Target, object? Dependent) TargetAndDependent { get; }
        public void Dispose();
    }
}

它可用於創建高級系統,例如複雜的快取系統或ConditionalWeakTable<TKey, TValue>類型​​的自定義版本。例如,它將被MVVM Toolkit中的WeakReferenceMessenger類型使用,以避免在廣播消息時分配記憶體。

可移植執行緒池

.NET 執行緒池已作為託管實現重新實現,現在用作.NET 6 中的默認執行緒池。我們進行此更改以使所有.NET 應用程式都可以訪問同一個執行緒池,而不管是否正在使用CoreCLR、Mono 或任何其他運行時。作為此更改的一部分,我們沒有觀察到或預期任何功能或性能影響。

RyuJIT

該團隊在此版本中對.NET JIT 編譯器進行了許多改進,在每個預覽帖子中都有記錄。這些更改中的大多數都提高了性能。這裡介紹了一些RyuJIT 的亮點。

動態 PGO

在.NET 6 中,我們啟用了兩種形式的PGO(配置文件引導優化):

  • 動態 PGO使用從當前運行中收集的數據來優化當前運行。
  • 靜態PGO依靠從過去運行中收集的數據來優化未來運行。

動態PGO 已經在文章前面的性能部分中介紹過。我將提供一個重新上限。

動態PGO 使JIT 能夠在運行時收集有關實際用於特定應用程式運行的程式碼路徑和類型的資訊。然後,JIT 可以根據這些程式碼路徑優化程式碼,有時會顯著提高性能。我們在測試和生產中都看到了兩位數的健康改進。有一組經典的編譯器技術在沒有PGO 的情況下使用JIT 或提前編譯都無法實現。我們現在能夠應用這些技術。熱/冷分離是一種這樣的技術,而去虛擬化是另一種技術。

要啟用動態PGO,請在應用程式將運行的環境中進行設置DOTNET\_TieredPGO=1

如性能部分所述,動態PGO 將TechEmpower JSON”MVC”套件每秒的請求數提高了26%(510K -&gt; 640K)。這是一個驚人的改進,無需更改程式碼。

我們的目標是在未來的.NET 版本中默認啟用動態PGO,希望在.NET 7 中啟用。我們強烈建議您在應用程式中嘗試動態PGO 並向我們提供回饋。

完整的 PGO

要充分利用Dynamic PGO,您可以設置兩個額外的環境變數:DOTNET\_TC\_QuickJitForLoops=1DOTNET\_ReadyToRun=0。 這確保了儘可能多的方法參與分層編譯。我們將此變體稱為 Full PGO 。與動態PGO 相比,完整PGO 可以提供更大的穩態性能優勢,但啟動時間會更慢(因為必須在第0 層運行更多方法)。

您不希望將此選項用於短期運行的無伺服器應用程式,但對於長期運行的應用程式可能有意義。

在未來的版本中,我們計劃精簡和簡化這些選項,以便您可以更簡單地獲得完整PGO 的好處並用於更廣泛的應用程式。

靜態 PGO

我們目前使用 靜態 PGO 來優化.NET 庫程式集,例如R2R(Ready To Run)附帶的程式集System.Private.CoreLib

靜態PGO 的好處是,在使用crossgen 將程式集編譯為R2R 格式時會進行優化。這意味著有運行時的好處而沒有運行時成本。這是非常重要的,也是PGO 對C++ 很重要的原因,例如。

循環對齊

記憶體對齊是現代計算中各種操作的共同要求。在.NET 5 中,我們開始在 32 位元組邊界對齊方法。在.NET 6 中,我們添加了一項執行自適應循環對齊的功能,該功能在具有循環的方法中添加NOP填充指令,以便循環程式碼從mod(16) 或mod(32) 記憶體地址開始。這些更改改進並穩定了.NET 程式碼的性能。

在下面的冒泡排序圖中,數據點1 表示我們開始在32 位元組邊界對齊方法的點。數據點2 表示我們也開始對齊內部循環的點。如您所見,基準測試的性能和穩定性都有很大提高。

硬體加速結構

結構是CLR 類型系統的重要組成部分。近年來,它們經常被用作整個.NET 庫中的性能原語。最近的例子ValueTaskValueTupleSpan<T>。記錄結構是一個新的例子。在.NET 5 和.NET 6 中,我們一直在提高結構的性能,部分原因是通過確保結構是局部變數、參數或方法的返回值時可以保存在超快速CPU 暫存器中)。這對於使用向量計算的API 特別有用。

穩定性能測量

團隊中有大量從未出現在部落格上的工程系統工作。這對於您使用的任何硬體或軟體產品都是如此。JIT 團隊開展了一個項目來穩定性能測量,目標是增加我們內部性能實驗室自動化自動報告的回歸值。這個項目很有趣,因為需要進行深入調查產品更改才能實現穩定性。它還展示了我們為保持和提高績效而衡量的規模。

此影像演示了不穩定的性能測量,其中性能在連續運行中在慢速和快速之間波動。x 軸是測試日期,y 軸是測試時間,以納秒為單位。到圖表末尾(提交這些更改後),您可以看到測量值穩定,結果最好。這張圖片展示了一個單一的測試。還有更多測試在dotnet/runtime #43227中被證明具有類似的行為。

即用型程式碼 /Crossgen 2

Crossgen2 是crossgen 工具的替代品。它旨在滿足兩個結果:

  • 讓crossgen開發更高效。
  • 啟用一組目前無法通過crossgen 實現的功能。

這種轉換有點類似於本機程式碼csc.exe 到託管程式碼Roslyn 編譯器。Crossgen2 是用C# 編寫的,但是它沒有像Roslyn 那樣公開一個花哨的API。

我們可能已經/已經為.NET 6 和7 計划了六個項目,這些項目依賴於crossgen2。矢量指令默認提議是我們希望為.NET 6 但更可能是.NET 7 進行的crossgen2 功能和產品更改的一個很好的例子。版本氣泡是另一個很好的例子。

Crossgen2 支援跨作業系統和架構維度的交叉編譯(因此稱為”crossgen”)。這意味著您將能夠使用單個構建機器為所有目標生成本機程式碼,至少與準備運行的程式碼相關。但是,運行和測試該程式碼是另一回事,為此您需要合適的硬體和作業系統。

第一步是用crossgen2編譯平台本身。我們使用.NET 6 完成了所有架構的任務。因此,我們能夠在此版本中淘汰舊的crossgen。請注意,crossgen2 僅適用於CoreCLR,而不適用於基於Mono 的應用程式(它們具有一組單獨的程式碼生成工具)。

這個項目——至少一開始——並不以性能為導向。目標是啟用更好的架構來託管RyuJIT(或任何其他)編譯器以離線方式生成程式碼(不需要或啟動運行時)。

你可能會說「嘿……如果是用C# 編寫的,難道你不需要啟動運行時來運行crossgen2 嗎?」 是的,但這不是本文中「離線」的含義。當crossgen2 運行時,我們不使用運行crossgen2 的運行時附帶的JIT 來生成準備運行(R2R) 程式碼. 那是行不通的,至少對於我們的目標來說是行不通的。想像一下crossgen2 在x64 機器上運行,我們需要為Arm64 生成程式碼。Crossgen2 將Arm64 RyuJIT(針對x64 編譯)載入為原生插件,然後使用它生成Arm64 R2R 程式碼。機器指令只是保存到文件中的位元組流。它也可以在相反的方向工作。在Arm64 上,crossgen2 可以使用編譯為Arm64 的x64 RyuJIT 生成x64 程式碼。我們使用相同的方法來針對x64 機器上的x64 程式碼。Crossgen2 會載入一個RyuJIT,它是為任何需要的配置而構建的。這可能看起來很複雜,但如果您想啟用無縫的交叉定位模型,它就是您需要的那種系統,而這正是我們想要的。

我們希望只在一個版本中使用術語「crossgen2」,之後它將替換現有的crossgen,然後我們將回到使用術語「crossgen」來表示「crossgen2」。

.NET 診斷:EventPipe

EventPipe 是我們用於在進程內或進程外輸出事件、性能數據和計數器的跨平台機制。從.NET 6 開始,我們已將實現從C++ 移至C。通過此更改,Mono 也使用EventPipe。這意味著CoreCLR 和Mono 都使用相同的事件基礎設施,包括.NET 診斷CLI 工具。

這一變化還伴隨著CoreCLR 的小幅減小:

大小之後 大小之前 差異
libcoreclr.so 7037856 – 7049408 -11552

我們還進行了一些更改,以提高 EventPipe 在負載下的吞吐量。在最初的幾個預覽版中,我們進行了一系列更改,從而使吞吐量提高了.NET 5 的2.06 倍:

對於這個基準,越高越好。.NET 6 是橙色線,.NET 5 是藍色線。

SDK

對.NET SDK 進行了以下改進。

.NET 6 SDK 可選工作負載的 CLI 安裝

.NET 6 引入了SDK 工作負載的概念。工作負載是可選組件,可以安裝在.NET SDK 之上以啟用各種場景。.NET 6 中的新工作負載是:.NET MAUI 和Blazor WebAssembly AOT 工作負載。我們可能會在.NET 7 中創建新的工作負載(可能來自現有的SDK)。工作負載的最大好處是減少大小和可選性。我們希望隨著時間的推移使SDK 變得更小,並且只安裝您需要的組件。這個模型對開發者機器有好處,對CI 來說甚至更好。

Visual Studio 用戶並不真正需要擔心工作負載。工作負載功能經過專門設計,以便像Visual Studio 這樣的安裝協調器可以為您安裝工作負載。可以通過CLI 直接管理工作負載。

工作負載功能公開了用於管理工作負載的多個動詞,包括以下幾個:

  • dotnet workload restore— 安裝給定項目所需的工作負載。
  • dotnet workload install— 安裝命名工作負載。
  • dotnet workload list— 列出您已安裝的工作負載。
  • dotnet workload update— 將所有已安裝的工作負載更新到最新的可用版本。

update動詞查詢更新nuget.org的工作負載清單、更新本地清單、下載已安裝工作負載的新版本,然後刪除所有舊版本的工作負載。這類似於apt update &amp;&amp; apt upgrade -y(用於基於Debian 的Linux 發行版)。將工作負載視為SDK 的私有包管理器是合理的。它是私有的,因為它僅適用於SDK 組件。我們將來可能會重新考慮這一點。這些dotnet workload命令在給定SDK 的上下文中運行。假設您同時安裝了.NET 6 和.NET 7。工作負載命令將為每個SDK 提供不同的結果,因為工作負載將不同(至少相同工作負載的不同版本)。

請注意,將NuGet.org 中的工作負載複製到您的SDK 安裝中,因此如果SDK 安裝位置受到保護(即在管理員/根位置),dotnet workload install則需要運行提升或使用sudo

內置 SDK 版本檢查

為了更容易跟蹤SDK 和運行時的新版本何時可用,我們向.NET 6 SDK 添加了一個新命令。

dotnet sdk check

它會告訴您是否有可用於您已安裝的任何.NET SDK、運行時或工作負載的更新版本。您可以在下圖中看到新體驗。

dotnet new

您現在可以在NuGet.org 中搜索帶有.dotnet new --search

模板安裝的其他改進包括支援切換以支援私有NuGet 源的授權憑據。--interactive

安裝CLI 模板後,您可以通過和檢查更新是否可用。--update-check--update-apply

NuGet 包驗證

包驗證工具使NuGet 庫開發人員能夠驗證他們的包是否一致且格式正確。

這包括:

  • 驗證版本之間沒有重大更改。
  • 驗證包對於所有特定於運行時的實現是否具有相同的公共API 集。
  • 確定任何目標框架或運行時適用性差距。

該工具是SDK 的一部分。使用它的最簡單方法是在項目文件中設置一個新屬性。

<EnablePackageValidation> true </EnablePackageValidation>

更多 Roslyn 分析儀

在.NET 5 中,我們提供了大約250 個帶有.NET SDK 的分析器。其中許多已經存在,但作為NuGet 包在帶外發送。我們為 .NET 6 添加了更多分析器

默認情況下,大多數新分析器都在資訊級別啟用。您可以通過如下配置分析模式在警告級別啟用這些分析器:<AnalysisMode>All</AnalysisMode>

我們為.NET 6 發布了我們想要的一組分析器(加上一些附加功能),然後將它們中的大多數做成了可供抓取的。社區添加了幾個實現,包括這些。

貢獻者 問題 標題
紐厄爾·克拉克 dotnet/運行時#33777 使用基於跨度的string.Concat
紐厄爾·克拉克 dotnet/運行時#33784 解析時優先string.AsSpan()string.Substring()
紐厄爾·克拉克 dotnet/運行時#33789 覆蓋Stream.ReadAsync/WriteAsync
紐厄爾·克拉克 dotnet/運行時#35343 替換為Dictionary&lt;,&gt;.Keys.ContainsContainsKey
紐厄爾·克拉克 dotnet/運行時#45552 使用代替String.EqualsString.Compare
梅克特雷爾 dotnet/運行時#47180 使用代替String.Contains(char)String.Contains(String)

感謝Meik TranelNewell Clark

為 Platform Compatibility Analyzer 啟用自定義防護

CA1416 平台兼容性分析器已經使用OperatingSystemRuntimeInformation中的方法識別平台防護,例如OperatingSystem.IsWindowsOperatingSystem.IsWindowsVersionAtLeast。但是,分析器無法識別任何其他保護可能性,例如快取在欄位或屬性中的平台檢查結果,或者在輔助方法中定義了複雜的平台檢查邏輯。

為了允許自定義守衛的可能性,我們添加了新屬性 SupportedOSPlatformGuardUnsupportedOSPlatformGuard使用相應的平台名稱和/或版本注釋自​​定義守衛成員。此注釋被平台兼容性分析器的流分析邏輯識別和尊重。

用法

    [UnsupportedOSPlatformGuard("browser")] // The platform guard attribute
#if TARGET_BROWSER
    internal bool IsSupported => false;
#else
    internal bool IsSupported => true;
#endif

    [UnsupportedOSPlatform("browser")]
    void ApiNotSupportedOnBrowser() { }

    void M1()
    {
        ApiNotSupportedOnBrowser();  // Warns: This call site is reachable on all platforms.'ApiNotSupportedOnBrowser()' is unsupported on: 'browser'

        if (IsSupported)
        {
            ApiNotSupportedOnBrowser();  // Not warn
        }
    }

    [SupportedOSPlatform("Windows")]
    [SupportedOSPlatform("Linux")]
    void ApiOnlyWorkOnWindowsLinux() { }

    [SupportedOSPlatformGuard("Linux")]
    [SupportedOSPlatformGuard("Windows")]
    private readonly bool _isWindowOrLinux = OperatingSystem.IsLinux() || OperatingSystem.IsWindows();

    void M2()
    {
        ApiOnlyWorkOnWindowsLinux();  // This call site is reachable on all platforms.'ApiOnlyWorkOnWindowsLinux()' is only supported on: 'Linux', 'Windows'.

        if (_isWindowOrLinux)
        {
            ApiOnlyWorkOnWindowsLinux();  // Not warn
        }
    }
}

結束

歡迎使用.NET 6。它是另一個巨大的.NET 版本,在性能、功能、可用性和安全性方面都有很多的改進。我們希望您能找到許多改進,最終使您在日常開發中更有效率和能力,並提高性能或降低生產中應用程式的成本。我們已經開始從那些已經開始使用.NET 6 的人那裡聽到好消息。

在Microsoft,我們還處於.NET 6 部署的早期階段,一些關鍵應用程式已經投入生產,未來幾周和幾個月內還會有更多應用程式推出。

.NET 6 是我們最新的LTS 版本。我們鼓勵每個人都轉向它,特別是如果您使用的是.NET 5。我們期待它成為有史以來採用速度最快的.NET 版本。

此版本是至少1000 人(但可能更多)的結果。這包括來自Microsoft 的.NET 團隊以及社區中的更多人。我試圖在這篇文章中包含許多社區貢獻的功能。感謝您抽出寶貴時間創建這些內容並完成我們的流程。我希望這次經歷是一次美好的經歷,並且更多的人會做出貢獻。

這篇文章是許多有才華的人合作的結果。貢獻包括團隊在整個發布過程中提供的功能內容、為此最終帖子創建的重要新內容,以及使最終內容達到您應得的品質所需的大量技術和散文更正。很高興為您製作它和所有其他帖子。

感謝您成為.NET 開發人員。