.NET 5.0 RC1 发布,离正式版发布仅剩两个版本,与 netty 相比更具竞争力
原文:http://dwz.win/Qf8
作者:Richard
翻譯:精致碼農-王亮
說明:
1. 本譯文并不是完全逐句翻譯的,存在部分語句我實在不知道如何翻譯或組織就根據個人理解用自己的話表述了。
2. 本文有不少超鏈接,由于微信公眾號和頭條平臺外鏈會被剔除 URL 地址,所以原來本是超鏈接的內容會顯示為純文本,如果你需要這些信息你可以移步到我的知乎和博客園閱讀(搜索精致碼農可找到我)。
今天我們發布了 .NET 5.0 Release Candidate 1 (RC1)。它是目前最接近 .NET 5.0 的一個版本,也是在 11 月正式發布之前的兩個 RC 版本中的第一個 RC 版本。RC1 是一個“上線”版本,表示你可以在生產環境中使用它了。
與此同時,我們一直在尋找最終正式版發布之前應該被修復的任何關鍵錯誤報告。我們需要你的反饋來幫助我們一起跨越 .NET 5.0 正式發布這道勝利的終點線。
我們今天也發布了 ASP.NET Core 和 EF Core 的 RC1 版本。
你可以下載適用于 Windows、macOS 和 Linux 的 .NET 5.0 版本:
安裝程序和二進制包
容器鏡像
快速安裝程序
發布說明
已知問題
GitHub Issue 跟蹤
你需要最新的預覽版 Visual Studio (包括 Visual Studio for Mac) 才能使用 .NET 5.0。
.NET 5.0 有很多改進,特別是單個文件應用程序、更小的容器映像、更強大的 JsonSerializer API、完整的可空引用類型標注、新的目標 Framework 名稱,以及對 Windows ARM64 的支持。在網絡庫、GC 和 JIT 中性能得到了極大的提高。我們花了很大的工作在 ARM64 的性能上,它有了更好的吞吐量和更小的二進制文件。.NET 5.0 包含了新的語言版本:C# 9.0 和 F# 5.0。
我們最近發布了一些關于 5.0 新功能深入介紹的文章,你可能想看一看這些文章:
F# 5 update for August
ARM64 Performance in .NET 5
Improvements in native code interop in .NET 5.0
Introducing the Half type!
App Trimming in .NET 5
Customizing Trimming in .NET 5
Automatically find latent bugs in your code with .NET 5
就像我在 .NET 5.0 預覽 8 文中所做的一樣,我選擇了一些特性來進行更深入的介紹,并讓你了解如何在實際使用中使用它們。這篇文章專門討論 C# 9 中的 System.Text.Json.JsonSerializer 和 records(記錄)。它們是獨立的特性,但也是很好的組合,特別是如果你花費大量時間為反序列化的 JSON 對象創建 POCO 類型。
C# 9 — 記錄
記錄(原文 Record)可能是 C# 9 中最重要的新特性。它們提供了廣泛的特性集(對一種語言類型來說),其中一些需要 RC1 或更高版本(如 record.ToString())。
譯注:為了閱讀更通順,對 Record 的翻譯,本譯文根據語境的情況,有的地方用的是“Record”,有的地方用的是“記錄”。因為在一些語境下把“Record”翻譯成“記錄”容易產生數據記錄的錯誤聯想。
最簡單的理解,記錄是不可變類型。在特性方面,它們最接近元組(Tuple),可以將它們視為具有屬性且不可變的自定義元組。在如今使用元組的多數情況下,記錄可以比元組提供更好更多的功能和使用場景。
在使用 C# 時,如果你使用命名類型會使你得到最好的體驗(相對于像元組這樣的特性)。靜態類型(static typing)是該語言的設計要點,記錄使小型類型更容易使用,并在整個應用程序中可以保證類型安全。
記錄是不可變數據類型
記錄使你能夠創建不可變的數據類型,這對于定義存儲少量數據的類型非常有用。
下面是一個記錄的例子,它存儲登錄用戶信息。
public?record?LoginResource(string?Username,?string?Password,?bool?RememberMe);它在語義上與下面的類相似(幾乎完全相同),我即將介紹這些差異。
public?class?LoginResource {public?LoginResource(string?username,?string?password,?bool?rememberMe){Username?=?username;Password?=?password;RememberMe?=?rememberMe;}public?string?Username?{?get;?init;?}public?string?Password?{?get;?init;?}public?bool?RememberMe?{?get;?init;?} }init 是一個新的關鍵字,它是 set 的替代。set 允許你在任何時候給屬性分配值,init 只允許在對象構造期間給屬性賦值,它是記錄不變性所依賴的基石。任何類型都可以使用 init,正如你在前面的類定義中看到的那樣,它并不局限于在記錄中使用。
private set 看起來類似于 init,private set 防止其他代碼(類型以外的代碼)改變數據。當類型(在構造之后)意外地改變屬性時,init 將產生編譯錯誤。private set 不能使數據不可變,因此當類型在構造后改變屬性值時,不會生成任何編譯錯誤或警告。
記錄是特殊的類
正如我剛才提到的,LoginResource 的記錄變體和類變體幾乎是相同的。類定義是記錄的一個語義相同的子集,記錄提供了更多特殊的行為。
為了讓我們的想法達成一致,如前所述,下面的比較是一個記錄和一個使用 init 代替 set 修飾屬性的類之間的區別。
有哪些共同點:
構造函數
不變性
復制語義(記錄本質是類)
有哪些不同點:
記錄相等是基于內容的,類相等是基于對象標識;
記錄提供了一個基于內容 GetHashCode() 實現;
記錄提供了一個IEquatable<T>的實現,它使用 GetHashCode() 唯一性作為行為機制,為記錄提供基于內容的相等語義;
記錄重寫(override)了 ToString(),打印的是記錄的內容。
記錄和(使用 init 的)類之間的差異可以在 LoginResource 作為記錄和 LoginResource 作為類的反編譯代碼中可以看到。
我將向你展示一些有差異的代碼:
using?System; using?System.Linq; using?static?System.Console;var?user?=?"Lion-O"; var?password?=?"jaga"; var?rememberMe?=?true; LoginResourceRecord?lrr1?=?new(user,?password,?rememberMe); var?lrr2?=?new?LoginResourceRecord(user,?password,?rememberMe); var?lrc1?=?new?LoginResourceClass(user,?password,?rememberMe); var?lrc2?=?new?LoginResourceClass(user,?password,?rememberMe);WriteLine($"Test?record?equality?--?lrr1?==?lrr2?:?{lrr1?==?lrr2}"); WriteLine($"Test?class?equality??--?lrc1?==?lrc2?:?{lrc1?==?lrc2}"); WriteLine($"Print?lrr1?hash?code?--?lrr1.GetHashCode():?{lrr1.GetHashCode()}"); WriteLine($"Print?lrr2?hash?code?--?lrr2.GetHashCode():?{lrr2.GetHashCode()}"); WriteLine($"Print?lrc1?hash?code?--?lrc1.GetHashCode():?{lrc1.GetHashCode()}"); WriteLine($"Print?lrc2?hash?code?--?lrc2.GetHashCode():?{lrc2.GetHashCode()}"); WriteLine($"{nameof(LoginResourceRecord)}?implements?IEquatable<T>:?{lrr1?is?IEquatable<LoginResourceRecord>}?"); WriteLine($"{nameof(LoginResourceClass)}??implements?IEquatable<T>:?{lrr1?is?IEquatable<LoginResourceClass>}"); WriteLine($"Print?{nameof(LoginResourceRecord)}.ToString?--?lrr1.ToString():?{lrr1.ToString()}"); WriteLine($"Print?{nameof(LoginResourceClass)}.ToString??--?lrc1.ToString():?{lrc1.ToString()}");public?record?LoginResourceRecord(string?Username,?string?Password,?bool?RememberMe);public?class?LoginResourceClass {public?LoginResourceClass(string?username,?string?password,?bool?rememberMe){Username?=?username;Password?=?password;RememberMe?=?rememberMe;}public?string?Username?{?get;?init;?}public?string?Password?{?get;?init;?}public?bool?RememberMe?{?get;?init;?} }注意:你將注意到 LoginResource 類型以 Record 和 Class 結束,該模式并不是新的命名約定,這樣命名只是為了在樣本中有相同類型的記錄和類變體,請不要這樣命名你的類。
此代碼的輸出如下:
rich@thundera?records?%?dotnet?run Test?record?equality?--?lrr1?==?lrr2?:?True Test?class?equality??--?lrc1?==?lrc2?:?False Print?lrr1?hash?code?--?lrr1.GetHashCode():?-542976961 Print?lrr2?hash?code?--?lrr2.GetHashCode():?-542976961 Print?lrc1?hash?code?--?lrc1.GetHashCode():?54267293 Print?lrc2?hash?code?--?lrc2.GetHashCode():?18643596 LoginResourceRecord?implements?IEquatable<T>:?True LoginResourceClass??implements?IEquatable<T>:?False Print?LoginResourceRecord.ToString?--?lrr1.ToString():?LoginResourceRecord?{?Username?=?Lion-O,?Password?=?jaga,?RememberMe?=?True?} Print?LoginResourceClass.ToString?--?lrc1.ToString():?LoginResourceClass記錄的語法
有多種用于聲明記錄的模式,用于滿足不同場景的使用。在玩過每個模式之后,你開始會對每種模式的好處有一個感性的認識。你還將看到,它們不是不同的語法,而是選項的連續體(continuum of options)。
第一個模式是最簡單的 —— 一行代碼 —— 但是提供的靈活性最小,它適用于具有少量必需屬性(必需屬性,即初始化時必需給作為參數的屬性傳值)的記錄。
以下用前面展示的 LoginResource 記錄作為此模式的一個示例。就這么簡單,一行代碼就是整個定義:
public?record?LoginResource(string?Username,?string?Password,?bool?RememberMe);構造遵循帶參數的構造函數的要求(包括允許使用可選參數):
var?login?=?new?LoginResource("Lion-O",?"jaga",?true);如果你喜歡,也可以用 target typing:
LoginResource?login?=?new("Lion-O",?"jaga",?true);下面這個語法使所有屬性都是可選的,為記錄提供了一個隱式無參數構造函數。
public?record?LoginResource {public?string?Username?{get;?init;}public?string?Password?{get;?init;}public?bool?RememberMe?{get;?init;} }使用對象初始化構造,可以像下面這樣:
LoginResource?login?=?new() {Username?=?"Lion-O",TemperatureC?=?"jaga" };如果你想讓這兩個屬性成為必需的,而另一個屬性是可選的,這最后一個模式如下所示:
public?record?LoginResource(string?Username,?string?Password) {public?bool?RememberMe?{get;?init;} }可以像下面這樣不指定 RememberMe 構造:
LoginResource?login?=?new("Lion-O",?"jaga");也可以指定 RememberMe 構造:
LoginResource?login?=?new("Lion-O",?"jaga") {RememberMe?=?true };不要認為記錄只用于不可變數據。你可以置入公開可變屬性,如下面的示例所示,該示例報告了關于電池的信息。Model 和 TotalCapacityAmpHours 屬性是不可變的,而 RemainingCapacityPercentange 是可變的。
using?System;Battery?battery?=?new?Battery("CR2032",?0.235) {RemainingCapacityPercentage?=?100 };Console.WriteLine?(battery);for?(int?i?=?battery.RemainingCapacityPercentage;?i?>=?0;?i--) {battery.RemainingCapacityPercentage?=?i; }Console.WriteLine?(battery);public?record?Battery(string?Model,?double?TotalCapacityAmpHours) {public?int?RemainingCapacityPercentage?{get;set;} }它輸出如下結果:
rich@thundera?recordmutable?%?dotnet?run Battery?{?Model?=?CR2032,?TotalCapacityAmpHours?=?0.235,?RemainingCapacityPercentage?=?100?} Battery?{?Model?=?CR2032,?TotalCapacityAmpHours?=?0.235,?RemainingCapacityPercentage?=?0?}無損式記錄修改
不變性提供了顯著的好處,但是您很快就會發現需要對記錄進行改變的情況。你怎么能在不放棄不變性的前提下做到這一點呢?with 表達式滿足了這一需求。它支持根據相同類型的現有記錄創建新記錄。你可以指定你想要的不同的新值,并且從現有記錄復制所有其他屬性。
讓我們把用戶名轉換成小寫,這是用戶名在我們假定的一個用戶數據庫中的存儲方式。但是,為了進行診斷,需要使用原始用戶名大小寫。假設以前面示例中的代碼為例,它可能像下面這樣:
LoginResource?login?=?new("Lion-O",?"jaga",?true); LoginResource?loginLowercased?=?lrr1?with?{Username?=?login.Username.ToLowerInvariant()};login 記錄沒有被更改,事實上這也是不允許的。轉換只影響了 loginLowercased,除了將小寫轉換為 loginLowercased 之外,其它與 login 是相同的。
我們可以使用內置的 ToString() 檢查 width 是否完成了預期的工作:
Console.WriteLine(login); Console.WriteLine(loginLowercased);此代碼輸出如下結果:
LoginResource?{?Username?=?Lion-O,?Password?=?jaga,?RememberMe?=?True?} LoginResource?{?Username?=?lion-o,?Password?=?jaga,?RememberMe?=?True?}我們可以進一步了解 with 是如何工作的,它將所有值從一條記錄復制到另一條記錄。這不是一個記錄依賴于另一個記錄的模型。事實上,with 操作完成后,兩個記錄之間就沒有關系了,只在對記錄的構建時有意義。這意味著對于引用類型,副本只是引用的副本;對于值類型,是復制值。
你可以在下面的代碼中看到這種語義:
Console.WriteLine($"Record?equality:?{login?==?loginLowercased}"); Console.WriteLine($"Property?equality:?Username?==?{login.Username?==?loginLowercased.Username};?Password?==?{login.Password?==?loginLowercased.Password};?RememberMe?==?{login.RememberMe?==?loginLowercased.RememberMe}");它輸出如下結果:
Record?equality:?False Property?equality:?Username?==?False;?Password?==?True;?RememberMe?==?True記錄的實例
對記錄進行擴展是很容易的。讓我們假設一個新的 LastLoggedIn 屬性,它可以直接添加到 LoginResource。那是個好的設想,記錄不像傳統的接口那樣脆弱,除非你想讓該新屬性在創建時作為構造函數所必需的參數。
在這個案例中,現在我想使 LastLoggedIn 是必需的。想象一下,代碼庫非常大,把這個修改反應到所有創建 LoginResource 的地方工作量是巨大的。相反,我們將用這個新屬性創建一個擴展 LoginResource 的新 Record。現有代碼將在 LoginResource 方面工作,新代碼將在新 Record 上工作,然后可以假設 LastLoggedIn 屬性已經賦值。根據常規繼承規則,接受 LoginResource 的代碼將同樣輕松地接受新的 Record。
這個新 Record 可以基于前面演示的任何 LoginResource 變體,它將基于以下內容:
public?record?LoginResource(string?Username,?string?Password) {public?bool?RememberMe?{get;?init;} }新的 Record 將是如下這樣的:
public?record?LoginWithUserDataResource(string?Username,?string?Password,?DateTime?LastLoggedIn)?:?LoginResource(Username,?Password) {public?int?DiscountTier?{get;?init};public?bool?FreeShipping?{get;?init}; }我將 LastLoggedIn 設置為一個必需的屬性,并利用這個機會添加了附加的且可選的屬性,這些屬性可能設置也可能沒有設置值。通過擴展 LoginResource 記錄,還定義了可選的 RememberMe 屬性。
記錄的構造輔助
其中一個不是很直觀的模式是建模輔助(modeling helpers),你希望使用它作為記錄構造的一部分(譯注:用來輔助創建記錄實例)。讓我們來換個體重測量的示例。體重的測量用的是一個聯網的秤,重量以公斤為單位,但是在某些情況下,體重需要以磅作為單位顯示。
可以使用以下記錄聲明:
public?record?WeightMeasurement(DateTime?Date,?int?Kilograms) {public?int?Pounds?{get;?init;}public?static?int?GetPounds(int?kilograms)?=>?kilograms?*?2.20462262; }對應的構造是這樣的:
var?weight?=?200; WeightMeasurement?measurement?=?new(DateTime.Now,?weight) {Pounds?=?WeightMeasurement.GetPounds(weight) };在本例中,需要說明的是 weight 是本地變量,不可能在對象初始化器中訪問 Kilograms 屬性。也有必要將 GetPounds 定義為靜態方法,因為不可能在對象初始化器中調用實例(它還未構造完成)方法。
記錄和可空性
語法上,記錄是具有可空性(Nullability)的對嗎?既然記錄是不可變的,那 null 從何而來呢?如果初始值就是 null,那就一直是 null,這樣的數據有什么意義呢?
讓我們來看一個沒有使用可空性的程序:
using?System; using?System.Collections.Generic;Author?author?=?new(null,?null);Console.WriteLine(author.Name.ToString());public?record?Author(string?Name,?List<Book>?Books) {public?string?Website?{get;?init;}public?string?Genre?{get;?init;}public?List<Author>?RelatedAuthors?{get;?init;} }public?record?Book(string?name,?int?Published,?Author?author);這個程序編譯時將拋出一個 NullReference 異常,因為 author.Name 是 null(譯者疑問:真的是編譯時報錯而不是運行時報錯嗎?期待大家親測)。
為了更進一步說明這一點,下面的代碼無法編譯通過,因為 author.Name 初始值為 null,然后是不能更改的,因為屬性是不可變的。
Author?author?=?new(null,?null); author.Name?=?"Colin?Meloy";我要更新我的 project 文件,以啟用可空性。
<Project?Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net5.0</TargetFramework><LangVersion>preview</LangVersion><Nullable>enable</Nullable></PropertyGroup></Project>我現在看到如下的一堆警告:
/Users/rich/recordsnullability/Program.cs(8,21):?warning?CS8618:?Non-nullable?property?'Website'?\n must?contain?a?non-null?value?when?exiting?constructor.?Consider?declaring?the?property?as?\n nullable.?[/Users/rich/recordsnullability/recordsnullability.csproj]我用我用可空修飾符更新了 Author 記錄,這些可空修飾符描述了我打算如何使用該記錄。
public?record?Author(string?Name,?List<Book>?Books) {public?string??Website?{get;?init;}public?string??Genre?{get;?init;}public?List<Author>??RelatedAuthors?{get;?init;} }我仍然得到了關于 null 的警告,之前看到的 Author 的 null 構造。
/Users/rich/recordsnullability/Program.cs(5,21):?warning?CS8625:?Cannot?convert?null?literal?\n to?non-nullable?reference?type.?[/Users/rich/recordsnullability/recordsnullability.csproj]這很好,因為這是我想防止的情況。現在,我將向你展示這個程序的一個更新版本,它很好地利用了可空性的好處。
using?System; using?System.Collections.Generic; using?System.Diagnostics.CodeAnalysis;Author?lord?=?new?Author("Karen?Lord") {Website?=?"https://karenlord.wordpress.com/",RelatedAuthors?=?new() };lord.Books.AddRange(new?Book[]{new?Book("The?Best?of?All?Possible?Worlds",?2013,?lord),new?Book("The?Galaxy?Game",?2015,?lord)} );lord.RelatedAuthors.AddRange(new?Author[]{new?("Nalo?Hopkinson"),new?("Ursula?K.?Le?Guin"),new?("Orson?Scott?Card"),new?("Patrick?Rothfuss")} );Console.WriteLine($"Author:?{lord.Name}"); Console.WriteLine($"Books:?{lord.Books.Count}"); Console.WriteLine($"Related?authors:?{lord.RelatedAuthors.Count}");public?record?Author(string?Name) {private?List<Book>?_books?=?new();public?List<Book>?Books?=>?_books;public?string??Website?{get;?init;}public?string??Genre?{get;?init;}public?List<Author>??RelatedAuthors?{get;?init;} }public?record?Book(string?name,?int?Published,?Author?author);這個程序編譯沒有出現警告。
你可能會對下面這句話感到疑惑:
lord.RelatedAuthors.AddRange(Author.RelatedAuthors 可以為空,編譯器可以看到 RelatedAuthors 屬性是在前面幾行設置的,因此它知道 RelatedAuthors 引用是非空的。
但是,想象一下如果這個程序是這樣的:
Author?GetAuthor() {return?new?Author("Karen?Lord"){Website?=?"https://karenlord.wordpress.com/",RelatedAuthors?=?new()}; }Author?lord?=?GetAuthor();當類型構造在一個單獨的方法中時,編譯器不能智能地知道 RelatedAuthors 是非空的。在這種情況下,將需要以下兩種模式之一:
lord.RelatedAuthors!.AddRange(或
if?(lord.RelatedAuthors?is?object) {lord.RelatedAuthors.AddRange(?... }這是一個關于記錄可空性的冗長演示,只是想說明它不會改變使用可空引用類型的任何體驗。
另外,您可能已經注意到,我將 Author 記錄上的 Books 屬性改為一個初始化的 get-only 屬性,而不是記錄構造函數中的一個必需參數。這是因為 Author 和 Books 之間存在一種循環關系(譯注:Author 含有List<Book>類型的導航屬性,Book 也包含 Author 類型的導航屬性)。不變性和循環引用可能會導致頭痛。在本例中,這是可以的,只是意味著需要在 Book 對象之前創建所有 Author 對象。因此,不可能在 Author 構造中提供一組完全初始化好的 Book 對象作為 Author 構建的一部分,我們所能期待的最好結果就是一個空的 List<Book>。因此,初始化一個作為 Author 構建的一部分的空 List<Book> 似乎是最好的選擇。沒有規則規定所有這些屬性都必須是 init 的形式,我(示例中)之所以這樣做是為了示范。
我們將轉移到 JSON 序列化的話題。這個帶有循環引用的示例與稍后將在 JSON 對象圖部分中的保存引用有關。JsonSerializer 支持循環引用的對象圖,但不支持帶有參數化構造函數的類型。你可以將 Author 對象序列化為 JSON,但不能將其反序列化為當前定義的 Author 對象。如果 Author 不是記錄或者沒有循環引用,那么序列化和反序列化都可以使用 JsonSerializer。
System.Text.Json
System.Text.Json 在 .NET 5.0 中得到了顯著的改進,提高了性能和可靠性,并使熟悉 Newtonsoft.Json 的人更容易采用它。它還支持將 JSON 對象反序列化為記錄,這是本文之前的文章介紹過的 C# 新特性。
如果你想將 System.Text.Json 作為 Newtonsoft.Json 的替代品,可以看這個 遷移指南,該指南闡明了這兩者 API 之間的關系。System.Text.Json 旨在涵蓋與 Newtonsoft.Json 相同的大多數場景,但是它并不是用來替代該流行的 Json 庫的,也不是為了實現與流行的 Json 庫相同的功能。我們試圖在性能和可用性之間保持平衡,并在設計選擇中偏向于性能。
HttpClient 擴展方法
JsonSerializer 擴展方法現在公開到 HttpClient 上了,極大地簡化了同時使用這兩個 API。這些擴展方法消除了復雜性,并為你處理各種場景,包括處理內容流和驗證內容媒體類型。Steve Gordon 很好地解釋了使用基于 System.Net.Http.Json 的 HttpClient 發送和接收 JSON 的好處。
下面的示例使用新的 GetFromJsonAsync<T>() 擴展方法將天氣預報的 JSON 數據反序列化為 Forecast 記錄。
using?System; using?System.Net.Http; using?System.Net.Http.Json;string?serviceURL?=?"https://localhost:5001/WeatherForecast"; HttpClient?client?=?new(); Forecast[]?forecasts?=?await?client.GetFromJsonAsync<Forecast[]>(serviceURL);foreach(Forecast?forecast?in?forecasts) {Console.WriteLine($"{forecast.Date};?{forecast.TemperatureC}C;?{forecast.Summary}"); }//?{"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"} public?record?Forecast(DateTime?Date,?int?TemperatureC,?int?TemperatureF,?string?Summary);這段代碼非常緊湊!它依賴于來自 C# 9 的頂層程序和記錄,以及新的 GetFromJsonAsync<T>() 擴展方法。如此近距離使用 foreach 和 await 可能會讓你懷疑我們是否會添加對 JSON 對象流的支持。是的,在未來的版本中。
譯注:上面作者所說的“近距離”我覺得意思是指反序列化時就近聲明需要的記錄類型,比單獨創建 Model 類放在單獨的文件中“近”許多。
你可以在你自己的機器上試試,下面的 .NET SDK 命令將使用 WebAPI 模板創建一個天氣預報服務。默認情況下,它的服務 URL 地址是:https://localhost:5001/WeatherForecast,與本示例中使用的 URL 相同。
rich@thundera?~?%?dotnet?new?webapi?-o?webapi rich@thundera?~?%?cd?webapi rich@thundera?webapi?%?dotnet?run先確保你已經運行了 dotnet dev-certs https --trust,否則客戶端和服務器之間的將不能正常握手通訊。如果有問題,請參見 Trust the ASP.NET Core HTTPS development certificate.。
然后你可以運行前面的例子:
rich@thundera?~?%?git?clone?https://gist.github.com/3b41d7496f2d8533b2d88896bd31e764.git?weather-forecast rich@thundera?~?%?cd?weather-forecast rich@thundera?weather-forecast?%?dotnet?run 9/9/2020?12:09:19?PM;?24C;?Chilly 9/10/2020?12:09:19?PM;?54C;?Mild 9/11/2020?12:09:19?PM;?-2C;?Hot 9/12/2020?12:09:19?PM;?24C;?Cool 9/13/2020?12:09:19?PM;?45C;?Balmy改進了對不可變類型的支持
定義不可變類型有多種模式,記錄只是最新的一種(比如下文示例中的一個 Struct 類型),JsonSerializer 現在支持不可變類型了。
在本例中,你將看到使用不可變結構類型的序列化:
using?System; using?System.Text.Json; using?System.Text.Json.Serialization;var?json?=?"{\"date\":\"2020-09-06T11:31:01.923395-07:00\",\"temperatureC\":-1,\"temperatureF\":31,\"summary\":\"Scorching\"}?"; var?options?=?new?JsonSerializerOptions() {PropertyNameCaseInsensitive?=?true,IncludeFields?=?true,PropertyNamingPolicy?=?JsonNamingPolicy.CamelCase }; var?forecast?=?JsonSerializer.Deserialize<Forecast>(json,?options);Console.WriteLine(forecast.Date); Console.WriteLine(forecast.TemperatureC); Console.WriteLine(forecast.TemperatureF); Console.WriteLine(forecast.Summary);var?roundTrippedJson?=?JsonSerializer.Serialize<Forecast>(forecast,?options);Console.WriteLine(roundTrippedJson);public?struct?Forecast{public?DateTime?Date?{get;}public?int?TemperatureC?{get;}public?int?TemperatureF?{get;}public?string?Summary?{get;}[JsonConstructor]public?Forecast(DateTime?date,?int?temperatureC,?int?temperatureF,?string?summary)?=>?(Date,?TemperatureC,?TemperatureF,?Summary)?=?(date,?temperatureC,?temperatureF,?summary); }注意:JsonConstructor 特性需要指定與 struct 一起使用的構造函數。對于類,如果只有一個構造函數,那么該特性就不是必需的,記錄也是如此。
它的輸出如下:
rich@thundera?jsonserializerimmutabletypes?%?dotnet?run 9/6/2020?11:31:01?AM -1 31 Scorching {"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"}支持記錄
JsonSerializer 對記錄的支持幾乎與我剛才對不可變類型的支持相同。這里我想展示的不同之處是將一個 JSON 對象反序列化為一個記錄,該記錄公開一個參數化的構造函數和一個可選的 init 屬性。
下面是一個包含了該記錄定義的程序:
using?System; using?System.Text.Json;Forecast?forecast?=?new(DateTime.Now,?40) {Summary?=?"Hot!" };string?forecastJson?=?JsonSerializer.Serialize<Forecast>(forecast); Console.WriteLine(forecastJson); Forecast??forecastObj?=?JsonSerializer.Deserialize<Forecast>(forecastJson); Console.Write(forecastObj);public?record?Forecast?(DateTime?Date,?int?TemperatureC) {public?string??Summary?{get;?init;} };它的輸出如下:
rich@thundera?jsonserializerrecords?%?dotnet?run {"Date":"2020-09-12T18:24:47.053821-07:00","TemperatureC":40,"Summary":"Hot!"} Forecast?{?Date?=?9/12/2020?6:24:47?PM,?TemperatureC?=?40,?Summary?=?Hot!?}改進了 Dictionary<K,V> 的支持
JsonSerializer 現在支持具有非字符串鍵的字典。你可以在下面的示例中看到它的樣子。在 .NET Core 3.0 中,這段代碼可以編譯,但會拋出 NotSupportedException 異常。
using?System; using?System.Collections.Generic; using?System.Text.Json;Dictionary<int,?string>?numbers?=?new?() {{0,?"zero"},{1,?"one"},{2,?"two"},{3,?"three"},{5,?"five"},{8,?"eight"},{13,?"thirteen"},{21,?"twenty?one"},{34,?"thirty?four"},{55,?"fifty?five"}, };var?json?=?JsonSerializer.Serialize<Dictionary<int,?string>>(numbers);Console.WriteLine(json);var?dictionary?=?JsonSerializer.Deserialize<Dictionary<int,?string>>(json);Console.WriteLine(dictionary[55]);它的輸出如下:
rich@thundera?jsondictionarykeys?%?dotnet?run {"0":"zero","1":"one","2":"two","3":"three","5":"five","8":"eight","13":"thirteen","21":"twenty?one","34":"thirty?four","55":"fifty?five"} fifty?five支持字段
JsonSerializer 現在支持字段,這個變化是由 @YohDeadfall 貢獻的,感謝他!
你可以在下面的示例中看到它的樣子,在 .NET Core 3.0 中,JsonSerializer 無法對使用字段的類型進行序列化或反序列化。對于具有字段且無法更改的現有類型來說,這是個問題,有了這個變化,這個問題就解決了。
using?System; using?System.Text.Json;var?json?=?"{\"date\":\"2020-09-06T11:31:01.923395-07:00\",\"temperatureC\":-1,\"temperatureF\":31,\"summary\":\"Scorching\"}?"; var?options?=?new?JsonSerializerOptions() {PropertyNameCaseInsensitive?=?true,IncludeFields?=?true,PropertyNamingPolicy?=?JsonNamingPolicy.CamelCase }; var?forecast?=?JsonSerializer.Deserialize<Forecast>(json,?options);Console.WriteLine(forecast.Date); Console.WriteLine(forecast.TemperatureC); Console.WriteLine(forecast.TemperatureF); Console.WriteLine(forecast.Summary);var?roundTrippedJson?=?JsonSerializer.Serialize<Forecast>(forecast,?options);Console.WriteLine(roundTrippedJson);public?class?Forecast{public?DateTime?Date;public?int?TemperatureC;public?int?TemperatureF;public?string?Summary; }它的輸出如下:
rich@thundera?jsonserializerfields?%?dotnet?run 9/6/2020?11:31:01?AM -1 31 Scorching {"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"}保留 JSON 對象圖中的引用
JsonSerializer 增加了對在 JSON 對象圖中保留(循環)引用的支持。它通過存儲在將 JSON 字符串反序列化回對象時可以重新構建的 id 來實現這一點。
using?System; using?System.Collections.Generic; using?System.Text.Json; using?System.Text.Json.Serialization;Employee?janeEmployee?=?new() {Name?=?"Jane?Doe",YearsEmployed?=?10 };Employee?johnEmployee?=?new() {Name?=?"John?Smith" };janeEmployee.Reports?=?new?List<Employee>?{?johnEmployee?}; johnEmployee.Manager?=?janeEmployee;JsonSerializerOptions?options?=?new() {//?NEW:?globally?ignore?default?values?when?writing?null?or?defaultDefaultIgnoreCondition?=?JsonIgnoreCondition.WhenWritingDefault,//?NEW:?globally?allow?reading?and?writing?numbers?as?JSON?stringsNumberHandling?=?JsonNumberHandling.AllowReadingFromString?|?JsonNumberHandling.WriteAsString,//?NEW:?globally?support?preserving?object?references?when?(de)serializingReferenceHandler?=?ReferenceHandler.Preserve,IncludeFields?=?true,?//?NEW:?globally?include?fields?for?(de)serializationWriteIndented?=?true,};string?serialized?=?JsonSerializer.Serialize(janeEmployee,?options); Console.WriteLine($"Jane?serialized:?{serialized}");Employee?janeDeserialized?=?JsonSerializer.Deserialize<Employee>(serialized,?options); Console.Write("Whether?Jane's?first?report's?manager?is?Jane:?"); Console.WriteLine(janeDeserialized.Reports[0].Manager?==?janeDeserialized);public?class?Employee {//?NEW:?Allows?use?of?non-public?property?accessor.//?Can?also?be?used?to?include?fields?"per-field",?rather?than?globally?with?JsonSerializerOptions.[JsonInclude]public?string?Name?{?get;?internal?set;?}public?Employee?Manager?{?get;?set;?}public?List<Employee>?Reports;public?int?YearsEmployed?{?get;?set;?}//?NEW:?Always?include?when?(de)serializing?regardless?of?global?options[JsonIgnore(Condition?=?JsonIgnoreCondition.Never)]public?bool?IsManager?=>?Reports?.Count?>?0; }性能
JsonSerializer 的性能在 .NET 5.0 中得到了顯著提高。Stephen Toub 在他的 .NET 5 的性能改進 一文中介紹了一些 JsonSerializer 的改進,我將在這里再介紹一些。
集合的(反)序列化
我們對大型集合做了顯著的改進(反序列化時為 1.15x-1.5x,序列化時為 1.5x-2.4x+)。你可以在 dotnet/runtime #2259 中更詳細地看到這些改進。
與 .NET 5.0 和 .NET Core 3.1 相比,List<int> (反)序列化的改進特別令人印象深刻,這些變化將在高性能應用程序中體現出來。
屬性查找 —— 命名約定
使用 JSON 最常見的問題之一是命名約定與 .NET 設計準則不匹配。JSON 屬性通常是 camelCase,.NET 屬性和字段通常是 PascalCase。你使用的 JSON 序列化器負責在命名約定之間架橋。這不是輕易就能做到的,至少對 .NET Core 3.1 來說不是。但在 .NET 5.0 中,這種實現成本現在可以忽略不計了。
允許缺少屬性和不區分大小寫的代碼在 .NET 5.0 中得到了極大的改進,在某些情況下它要快 1.75 倍。
下面是一個簡單的四屬性測試類的基準測試,它的屬性名為大于 7 字節。
3.1?性能 |????????????????????????????Method?|???????Mean?|???Error?|??StdDev?|?????Median?|????????Min?|????????Max?|??Gen?0?|?Gen?1?|?Gen?2?|?Allocated?| |----------------------------------?|-----------:|--------:|--------:|-----------:|-----------:|-----------:|-------:|------:|------:|----------:| |?CaseSensitive_Matching????????????|???844.2?ns?|?4.25?ns?|?3.55?ns?|???844.2?ns?|???838.6?ns?|???850.6?ns?|?0.0342?|?????-?|?????-?|?????224?B?| |?CaseInsensitive_Matching??????????|???833.3?ns?|?3.84?ns?|?3.40?ns?|???832.6?ns?|???829.4?ns?|???841.1?ns?|?0.0504?|?????-?|?????-?|?????328?B?| |?CaseSensitive_NotMatching(Missing)|?1,007.7?ns?|?9.40?ns?|?8.79?ns?|?1,005.1?ns?|???997.3?ns?|?1,023.3?ns?|?0.0722?|?????-?|?????-?|?????464?B?| |?CaseInsensitive_NotMatching???????|?1,405.6?ns?|?8.35?ns?|?7.40?ns?|?1,405.1?ns?|?1,397.1?ns?|?1,423.6?ns?|?0.0626?|?????-?|?????-?|?????408?B?|5.0?性能 |????????????????????????????Method?|?????Mean?|???Error?|??StdDev?|???Median?|??????Min?|??????Max?|??Gen?0?|?Gen?1?|?Gen?2?|?Allocated?| |----------------------------------?|---------:|--------:|--------:|---------:|---------:|---------:|-------:|------:|------:|----------:| |?CaseSensitive_Matching????????????|?799.2?ns?|?4.59?ns?|?4.29?ns?|?801.0?ns?|?790.5?ns?|?803.9?ns?|?0.0985?|?????-?|?????-?|?????632?B?| |?CaseInsensitive_Matching??????????|?789.2?ns?|?6.62?ns?|?5.53?ns?|?790.3?ns?|?776.0?ns?|?794.4?ns?|?0.1004?|?????-?|?????-?|?????632?B?| |?CaseSensitive_NotMatching(Missing)|?479.9?ns?|?0.75?ns?|?0.59?ns?|?479.8?ns?|?479.1?ns?|?481.0?ns?|?0.0059?|?????-?|?????-?|??????40?B?| |?CaseInsensitive_NotMatching???????|?783.5?ns?|?3.26?ns?|?2.89?ns?|?783.5?ns?|?779.0?ns?|?789.2?ns?|?0.1004?|?????-?|?????-?|?????632?B?|TechEmpower 改進
譯注:TechEmpower 是一家主要做基準測試的公司,它會定期提供各種 Web 應用程序框架性能指標的測試和比較,覆蓋了許多的語言框架,包括 C#,Go,Python,Java,Ruby,PHP 等。測試基于云和物理硬件,測試的性能則包括純文本響應、序列化 JSON 對象、單個/多個數據庫查詢、數據庫更新、Fortunes 測試等等。
我們在 TechEmpower 基準測試中花費了大量的精力來改進 .NET 的性能。使用 TechEmpower JSON 基準來驗證這些 JsonSerializer 改進是有意義的。現在性能提高了約 19%,一旦我們將條目更新到 .NET 5.0 將提高 .NET 在基準測試中的排行位置。這個版本的目標是與 netty 相比更具競爭力,netty 是常見的 Java Webserver。
在 dotnet/runtime #37976 中詳細介紹了這些更改和性能度量。這里有兩套基準,第一個是使用團隊維護的 JsonSerializer 性能基準測試來驗證性能。觀察到有約 8% 的改善;第二個是 TechEmpower 的,它測量了滿足 TechEmpower JSON 基準測試要求的三種不同方法。我們在官方基準測試中使用的是SerializeWithCachedBufferAndWriter。
如果我們看一下 Min 列,我們可以做一些簡單的數學計算:153.3/128.6 = ~1.19,有了 19% 的提升。
結束
我希望你喜歡本文對記錄和 JsonSerializer 的深入介紹,它們只是 .NET 5.0 眾多改進中的兩個。這篇預覽 8 的文章涵蓋了更多的新特性,這為 5.0 的價值提供了更廣闊的視角。
正如你所知道的,我們目前階段沒有在 .NET 5.0 中繼續添加新特性了。我利用后面的預覽和 RC 版本發布的文章來涵蓋我們已經添加的所有功能的介紹。你希望我在 RC2 發布的博客文章中介紹哪些內容?我想從你們那知道我應該關注什么。
請在評論中分享你使用 RC1 的體驗,感謝所有安裝了 .NET 5.0 的人,我們感謝到目前為止我們收到的所有參與和反饋。
總結
以上是生活随笔為你收集整理的.NET 5.0 RC1 发布,离正式版发布仅剩两个版本,与 netty 相比更具竞争力的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java国家/地区使用限制条款引发争议
- 下一篇: 初识ABP vNext(10):ABP设