.NET 6 史上最全攻略

語言: CN / TW / HK

歡迎使用.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 -\> 640K)。

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

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

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

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

```csharp private IEnumerator _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 寫入新文件。

```csharp 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。

```csharp async Task AllOrNothingAsync(string path, IReadOnlyList> 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# 項目中。有關詳細信息,請參閲隱式使用

文件範圍的命名空間

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

新語法是單個的一行:

```csharp namespaceMyNamespace;

classMyClass{...}// Not indented ```

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

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

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

記錄結構

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

csharp 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 運行的環境。

```csharp 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 上的體驗是相似的,並且還展示了架構目標。

```csharp 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

csharp $ 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: http://[::]:80","State":{"Message":"Now listening on: http://[::]:80","address":"http://[::]: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 一樣):

csharp $ 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: http://[::]: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 可觀察性場景的支持。

圖書館測量記錄示例

csharp 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"));

聽力示例

csharp 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 窗體應用程序

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

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

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

csharp 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來簡化在具體類型未知的通用上下文中處理向量。感謝霍耀源

重載支持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。簡而言之,源代碼生成器提供了一種在編譯時為您提供不同實現的方法,以使運行時體驗更好。

給定一個簡單的類型:

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

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

```csharp using System.Text.Json.Serialization;

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

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

```csharp 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值提供特殊處理。

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

static async IAsyncEnumerable 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方法,您可以在以下示例中看到。

```csharp 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(stream)) { Console.WriteLine(item); } ```

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

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

```csharp 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(stream); await foreach (int item in result.Data) { Console.WriteLine(item); }

public class MyPoco { public IAsyncEnumerable Data { get; set; } } ```

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

System.Text.Json:可寫 DOM 功能

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

這個新的 API 有以下好處:

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

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

```csharp // Parse a JSON object JsonNode jNode = JsonNode.Parse("{"MyProperty":42}"); int value = (int)jNode["MyProperty"]; Debug.Assert(value == 42); // or value = jNode["MyProperty"].GetValue(); 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() == 11); ```

ReferenceHandler.IgnoreCycles

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

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

```csharp 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%。

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

csharp 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方案,如以下示例所示。

csharp 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,以簡化應用程序設置(例如,配置關閉超時):

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

host.Run(); ```

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

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

host.Run(); ```

Microsoft.Extensions.DependencyInjection — CreateAsyncScope API

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

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

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

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

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

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

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

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

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

csharp 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方法的實現。

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

csharp 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

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

```csharp 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方法允許使用鍵選擇器查找最大或最小元素,如下例所示。

csharp 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方法現在支持組合三個枚舉,如以下示例所示。

```csharp 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>

```csharp // creates a priority queue of strings with integer priorities var pq = new PriorityQueue();

// 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:

csharp 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 時區

這種改進具有以下好處:

  • 使用時的隱式轉換(https://github.com/dotnet/runtime/pull/49412)TimeZoneInfo.FindSystemTimeZoneById
  • TimeZoneInfo通過: TryConvertIanaIdToWindowsIdTryConvertWindowsIdToIanaIdHasIanaId(https://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作為參數。(https://github.com/dotnet/runtime/issues/50256)
  • TimeZoneInfo.AdjustmentRule還獲得了在Unix 上加載時區的各種修復(https://github.com/dotnet/runtime/pull/49733), (https://github.com/dotnet/runtime/pull/50131)

改進了對 Windows ACL 的支持

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

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

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

對於Mutex

```csharp 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

```csharp 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

```csharp 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 表面

csharp 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 -\> 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\<,\>.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使用相應的平台名稱和/或版本註釋自​​定義守衞成員。此註釋被平台兼容性分析器的流分析邏輯識別和尊重。

用法

```csharp [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 開發人員。