1. 程式人生 > >【翻譯】.NET 5 RC1釋出

【翻譯】.NET 5 RC1釋出

9月14日,.NET5釋出了(Release Candidate)RC1版本,RC的意思是指我們可以進行使用,並且RC版本得到了支援,該版本很接近.NET5.0的最終版本,也是11月正式版本之前兩個RC版本中的其中一個。目前,開發團隊正在尋找在.NET5釋出之前剩餘的bug,當然他們也希望我們的反饋以幫助他們順利的完成.NET5的開發計劃。 開發團隊在今天還發布了[ASP.NET Core](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-5-release-candidate-1/?WT.mc_id=DOP-MVP-5003855)和[EF Core](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-efcore-5-0-rc1/?WT.mc_id=DOP-MVP-5003855)的RC1版本。 現在我們可以進行下載用於Windows、macOS和Linux的[.NET5](https://dotnet.microsoft.com/download/dotnet/5.0/?WT.mc_id=DOP-MVP-5003855) - [Installers and binaries](https://dotnet.microsoft.com/download/dotnet/5.0/?WT.mc_id=DOP-MVP-5003855) - [Container images](https://hub.docker.com/_/microsoft-dotnet) - [Snap installer](https://snapcraft.io/dotnet-sdk) - [Release notes](https://github.com/dotnet/core/blob/master/release-notes/5.0/preview/5.0.0-rc.1.md) - [Known issues](https://github.com/dotnet/core/blob/master/release-notes/5.0/5.0-known-issues.md) - [GitHub issue tracker](https://github.com/dotnet/core/issues/5200) 如果要使用.NET5,我們需要使用最新的[Visual Studio](https://visualstudio.microsoft.com/vs/preview/)預覽版(包括[Visual Studio for Mac](https://visualstudio.microsoft.com/vs/preview/)) 在.NET5中有許多的改進,特別是對單檔案可執行應用程式、更小的容器映像、更強大的JsonSerializer api、BCL nullable reference type annotated、新target framework names,以及對Windows ARM64的支援。在.NET庫中,GC和JIT的效能都得到了極大的提升,ARM64是效能優化的重點,它為我們帶來了更好的吞吐量和更小的二進位制檔案。.NET5.0包含了新的語言版本,[C#9](https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/?WT.mc_id=DOP-MVP-5003855)和[F#5.0](https://devblogs.microsoft.com/dotnet/f-5-update-for-august/?WT.mc_id=DOP-MVP-5003855)。 下面還有他們最近釋出的一些有關於.NET5.0新功能的文章,大家可以閱讀一下: - [F# 5 update for August](https://devblogs.microsoft.com/dotnet/f-5-update-for-august/?WT.mc_id=DOP-MVP-5003855) - [ARM64 Performance in .NET 5](https://devblogs.microsoft.com/dotnet/arm64-performance-in-net-5/?WT.mc_id=DOP-MVP-5003855) - [Improvements in native code interop in .NET 5.0](https://devblogs.microsoft.com/dotnet/improvements-in-native-code-interop-in-net-5-0/?WT.mc_id=DOP-MVP-5003855) - [Introducing the Half type!](https://devblogs.microsoft.com/dotnet/introducing-the-half-type/?WT.mc_id=DOP-MVP-5003855) - [App Trimming in .NET 5](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/?WT.mc_id=DOP-MVP-5003855) - [Customizing Trimming in .NET 5](https://devblogs.microsoft.com/dotnet/customizing-trimming-in-net-core-5/?WT.mc_id=DOP-MVP-5003855) - [Automatically find latent bugs in your code with .NET 5](https://devblogs.microsoft.com/dotnet/automatically-find-latent-bugs-in-your-code-with-net-5/?WT.mc_id=DOP-MVP-5003855) 其實就像在[.NET5 Preview8](https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-8/?WT.mc_id=DOP-MVP-5003855)中一樣,在本章還是像上一章一樣選擇了一些特性來進行深入的研究介紹,在本章中將深入的討論C#9中新特性`records `和`System.Text.Json.JsonSerializer`,它們是獨立的特性,但也是很好的一個組合,特別是在我們花費一些時間去為反序列化的JSON物件設計POCO型別時。 ## C# 9 — Records `Records`可能是[c#9](https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md#c-9)中最重要的一個新特性,它們提供了一個廣泛的特性集(對於一種語言型別),其中一些需要RC1或更高的版本(如record.ToString())。 將`records`看作不可變類是最簡單的方式,在特性方面,它們很接近元組(Tuple),可以將他們視為具有屬性和不可變性的自定義元組。在今天使用元組的許多情況下,records可以更好的提供這些元組。 如果你正在使用C#,你會得到最好的體驗,如果你使用命名型別(相對於像元組這樣的特性)。靜態型別是該語言主要的設計要點,records使小型型別更容易使用,並在整個應用程式中利用型別安全。 ### Records are immutable data types Records使我們能夠建立不可變的資料型別,這對於定義儲存少量資料的型別非常有用。 下面是一個records的示例,它儲存登入使用者資訊. ``` 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只允許在物件構建期間進行屬性的賦值操作,它是`records`的不變性所依賴的基礎,任何型別都可以使用`init`。正如我們在前面的定義中所看到的那樣,它不是特定於`records`的。 private set看起來類似於init;private set防止其他程式碼(型別以外的程式碼)改變資料,當型別(在構建之後)意外的改變屬性時,init將在編譯器生成時返回錯誤。private set並非旨在為不可變資料建模,因此當型別在構造後使屬性值發生衝突時,private set不會產生任何編輯器錯誤或者警告。 ### Records are specialized classes 正如上面提到的`LoginResource`的records的變數和類變數幾乎是相同的,類定義是記錄的一個語義相同的子集,records 提供了更多的、專門的行為。 下面是比較一個`record`和一個使用`init`而不是set作為屬性類之間的比較。 有什麼相同? - Construction - Immutability - Copy semantics (records are classes under the hood) 有什麼不同? - records相等性是基於內容的。基於物件標識的類相等性 - records提供了一個GetHashCode()實現,它基於record內容 - records提供一個IEquatable實現。它使用唯一的GetHashCode()行為作為機制,為record提供基於內容的相等語義。 - 覆蓋Record ToString()以列印record內容。 record和類(使用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: {lrr1 is IEquatable} "); WriteLine($"{nameof(LoginResourceClass)} implements IEquatable: {lrr1 is IEquatable}"); 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結束。該模式並不是新的命名模式的規範,這樣命名只是為了我們在程式碼片段中有相同型別的record和類變數。請不要這樣命名我們的型別。 如下是上面程式碼的輸出內容 ``` 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: True LoginResourceClass implements IEquatable: False Print LoginResourceRecord.ToString -- lrr1.ToString(): LoginResourceRecord { Username = Lion-O, Password = jaga, RememberMe = True } Print LoginResourceClass.ToString -- lrc1.ToString(): LoginResourceClass ``` ### Record syntax 有多種用於宣告`records`的用例,在使用過每種方式後,我們就會對每一種模式的好處有所瞭解,我們還能看到不同方式,他們不是不同的語法而是多種選擇。 第一個方式是最簡單的,但是它的靈活性比較小,它適用於具有少量必需屬性的`records`。 下面是前面顯示的LoginResource record,作為此模式的一個示例。這一行是的定義 ``` public record LoginResource(string Username, string Password, bool RememberMe); ``` 構造遵循具有引數的建構函式的要求(包括允許使用可選引數)。 ``` var login = new LoginResource("Lion-O", "jaga", true); ``` 還可以使用目標型別。 ``` LoginResource login = new("Lion-O", "jaga", true); ``` 下一個語法使所有屬性都是可選的。為record提供了一個隱式無引數建構函式。 ``` 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 }; ``` 如果說我們不認為`record`只用於不可變資料,那麼我們可以選擇公開可變屬性,如下程式碼片段所示,該片段展示了關於電池的資訊。Model和TotalCapacityAmpHours屬性是不可變的,而剩餘的容量百分比是可變的。 ``` 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 } ``` ### Non-destructive record mutation 不變性是給我們帶來了很多的好處,但是我們也很快的發現了需要修改`record`的情況,在不放棄`record`的情況下,我們該如何處理這種情況呢?`with`表示式可以滿足這些需求,它可以根據相同型別的現有record來建立新record,我們可以指定想要的不同的新值,並從現有的record中複製所有其他屬性. 現在我們有個需求就是將使用者名稱轉換為小寫,這樣的情況下我們才可以將其儲存到我們的資料庫中,如果說處理這個需求我們可能會像如下程式碼片段中這樣去處理: ``` LoginResource login = new("Lion-O", "jaga", true); LoginResource loginLowercased = lrr1 with {Username = login.Username.ToLowerInvariant()}; ``` 登入record沒有被更改,事實上,這是不可能的,轉換隻影響了`loginLowercased`,除了小寫轉換為`loginLowercased`之外其他與登入相同。 我們可以使用內建的ToString()覆蓋檢查`with`是否完成了預期的工作。 ``` Console.WriteLine(login); Console.WriteLine(loginLowercased); ``` 下面程式碼是輸出 ``` LoginResource { Username = Lion-O, Password = jaga, RememberMe = True } LoginResource { Username = lion-o, Password = jaga, RememberMe = True } ``` 我們可以進一步的瞭解`with`的工作原理,它將所有的值從一條record複製到另一條record。這不是一個record依賴於另一個record的委託模型。事實上`with`操作完成後,兩個record之間就沒有關係了,只對record的構建有意義,這就意味著對於引用型別,副本只是引用副本。對於值型別,複製值. 您可以使用以下程式碼檢視該語義。 ``` 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 ``` ### Record inheritance 擴充套件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`設定為一個必須的屬性,並且也增加了可選的屬性 ### Modeling record construction 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) }; ``` 在本例中,有必要將權重指定為local。不可能在物件初始化器中訪問公斤屬性。還需要將GetPounds定義為靜態方法。不可能在物件初始化器中呼叫例項方法(對於正在構造的型別)。 ### Records and Nullability 一切都是不可變的,那麼空值從何而來?不完全是。不可變屬性可以是null,並且在這種情況下將始終是null。 讓我們看看另一個沒有啟用可空性的程式。 ``` using System; using System.Collections.Generic; Author author = new(null, null); Console.WriteLine(author.Name.ToString()); public record Author(string Name, List Books) { public string Website {get; init;} public string Genre {get; init;} public List RelatedAuthors {get; init;} } public record Book(string name, int Published, Author author); ``` 這個程式將編譯並丟擲一個NullReference異常,這是由於取消引用author.Name為空。 為了進一步說明這一點,將不編譯以下內容。author.Name 初始化為null,然後不能更改,因為屬性是不可變的。 ``` Author author = new(null, null); author.Name = "Colin Meloy"; ``` 下面啟動可空性 ``` Exe
net5.0 preview enable
``` 下面我們能看到一堆這樣的警告 ``` /Users/rich/recordsnullability/Program.cs(8,21): warning CS8618: Non-nullable property 'Website' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [/Users/rich/recordsnullability/recordsnullability.csproj] ``` 用null註釋更新了Author record,這些註釋描述了我打算使用的record。 ``` public record Author(string Name, List Books) { public string? Website {get; init;} public string? Genre {get; init;} public List? RelatedAuthors {get; init;} } ``` 仍然得到了對null的警告,null構造的Author之前看到。 ``` /Users/rich/recordsnullability/Program.cs(5,21): warning CS8625: Cannot convert null literal 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 _books = new(); public List Books => _books; public string? Website {get; init;} public string? Genre {get; init;} public List? RelatedAuthors {get; init;} } public record Book(string name, int Published, Author author); ``` 該程式在編譯時不會出現可空的警告。 大家可能對下面這句有疑惑 ``` lord.RelatedAuthors.AddRange( ``` Author.RelatedAuthors可以為null。 編譯器可以看到,RelatedAuthors屬性的設定只是前面幾行,因此它知道RelatedAuthors引用將為非null。 但是,想象一下這個程式看起來是這樣的。 ``` Author GetAuthor() { return new Author("Karen Lord") { Website = "https://karenlord.wordpress.com/", RelatedAuthors = new() }; } Author lord = GetAuthor(); ``` 編譯器沒有流程分析技巧,無法知道當型別構造在單獨的方法中時,RelatedAuthor將為非空。 在這種情況下,將需要以下兩種模式之一 ``` lord.RelatedAuthors!.AddRange( ``` or ``` if (lord.RelatedAuthors is object) { lord.RelatedAuthors.AddRange( ... } ``` 這是一個關於記錄可空性的冗長演示,只是為了說明它不會改變使用可空引用型別的任何體驗。 另外,您可能已經注意到,我將Author record上的Books屬性移動為初始化的get-only屬性,而不是記錄建構函式中的必需引數。 這是由於作者與書籍之間存在迴圈關係。 不變性和迴圈引用可能會引起頭痛。 在這種情況下可以,並且僅表示需要在Book物件之前建立所有Author物件。 結果,無法提供完全初始化的Book物件集作為Author結構的一部分。 作為Author結構的一部分,我們可以期望的最好的是一個空的List 。 結果,初始化空的List 作為Author結構的一部分似乎是最佳選擇。 沒有規則要求所有這些屬性都必須是init樣式。這樣做只是為了演示該行為。 我們將過渡到談論JSON序列化。 這個帶有迴圈引用的示例與不久之後的在JSON物件圖中儲存引用有關。 JsonSerializer支援帶有迴圈引用的物件圖,但不支援帶有引數化建構函式的型別。 您可以將Author物件序列化為JSON,但不能序列化為當前定義的Author物件。 如果Author不是記錄或沒有迴圈引用,那麼JsonSerializer可以同時進行序列化和反序列化。 ## System.Text.Json .NET 5.0中對System.Text.Json進行了顯著改進,以提高效能,可靠性,當然如果熟悉Newtonsoft.Json那麼用起來更容易, 它還包括對將JSON物件反序列化為記錄的支援,本文前面已介紹了新的C#功能 如果要使用System.Text.Json替代Newtonsoft.Json,則應檢視[遷移指南](https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to/)。 該指南闡明瞭這兩個API之間的關係。 System.Text.Json旨在涵蓋與Newtonsoft.Json相同的許多場景,但並不旨在替代流行的JSON庫或與流行的JSON庫實現功能對等。 我們嘗試在效能和可用性之間保持平衡,並在設計選擇中偏向效能。 ### HttpClient extension methods [JsonSerializer擴充套件方法](https://github.com/dotnet/runtime/issues/32937)現在在HttpClient上公開,並且極大地簡化了同時使用這兩個api。這些擴充套件方法消除了複雜性,併為您處理各種場景,包括處理內容流和驗證內容媒體型別。Steve Gordon很好地解釋了使用帶有[System.Net.Http.Json的HttpClient傳送和接收JSON的好處](https://www.stevejgordon.co.uk/sending-and-receiving-json-using-httpclient-with-system-net-http-json)。 下面的示例使用新的[GetFromJsonAsync()](https://docs.microsoft.com/zh-cn/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync?view=net-5.0#System_Net_Http_Json_HttpClientJsonExtensions_GetFromJsonAsync__1_System_Net_Http_HttpClient_System_String_System_Threading_CancellationToken_)擴充套件方法將天氣預報JSON資料反序列化為預報記錄。 ``` 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(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的頂級程式和record,以及新的GetFromJsonAsync()擴充套件方法。在foreach和await的使用中可能大家會懷疑是否對流JSON物件的支援,在未來版本中是支援的。 大家可以在自己的機器上試試。下面的.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——首先信任,否則客戶端和伺服器之間的握手將不起作用。如果有問題,請參見[信任ASP.NET Core HTTPS開發證書](https://docs.microsoft.com/aspnet/core/security/enforcing-ssl?view=aspnetcore-5.0&tabs=visual-studio#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos)。 然後可以執行前面的示例。 ``` 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 ``` ### Improved support for immutable types 其實定義不可變型別有多種方式,`records`只是最新的一種,`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(json, options); Console.WriteLine(forecast.Date); Console.WriteLine(forecast.TemperatureC); Console.WriteLine(forecast.TemperatureF); Console.WriteLine(forecast.Summary); var roundTrippedJson = JsonSerializer.Serialize(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一起使用的建構函式,對於類,如果只有一個建構函式,那麼屬性就不是必須的,與records相同。 輸出內容: ``` 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"} ``` ### Support for records JsonSerializer對records的支援與上面展示的不可變型別的支援幾乎相同,我想在這裡顯示的區別是將JSON物件反序列化為一條records,該records公開了引數化的建構函式和可選的init屬性。 在下面程式碼片段中包含了對records的定義: ``` using System; using System.Text.Json; Forecast forecast = new(DateTime.Now, 40) { Summary = "Hot!" }; string forecastJson = JsonSerializer.Serialize(forecast); Console.WriteLine(forecastJson); Forecast? forecastObj = JsonSerializer.Deserialize(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! } ``` ### Improved Dictionary support JsonSerializer現在[支援具有非字串鍵的字典](https://github.com/dotnet/runtime/issues/30618)。我們可以在下面的示例中看到它的樣子。在.NET Core 3.0中,這段程式碼可以編譯,但會丟擲NotSupportedException異常。 ``` using System; using System.Collections.Generic; using System.Text.Json; Dictionary 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>(numbers); Console.WriteLine(json); var dictionary = JsonSerializer.Deserialize>(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 ``` ### Support for fields JsonSerializer現在支援欄位。 我們可以在下面的示例中看到它的樣子。在.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(json, options); Console.WriteLine(forecast.Date); Console.WriteLine(forecast.TemperatureC); Console.WriteLine(forecast.TemperatureF); Console.WriteLine(forecast.Summary); var roundTrippedJson = JsonSerializer.Serialize(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"} ``` ### Preserving references in JSON object graphs 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 { johnEmployee }; johnEmployee.Manager = janeEmployee; JsonSerializerOptions options = new() { // NEW: globally ignore default values when writing null or default DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, // NEW: globally allow reading and writing numbers as JSON strings NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString, // NEW: globally support preserving object references when (de)serializing ReferenceHandler = ReferenceHandler.Preserve, IncludeFields = true, // NEW: globally include fields for (de)serialization WriteIndented = true,}; string serialized = JsonSerializer.Serialize(janeEmployee, options); Console.WriteLine($"Jane serialized: {serialized}"); Employee janeDeserialized = JsonSerializer.Deserialize(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 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; } ``` ### Performance 在.NET 5.0中,JsonSerializer的效能得到了顯著改善。 Stephen Toub在.NET 5中的[Performance Improvements](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/#json)中涵蓋了JsonSerializer的一些改進。 我會在這裡再介紹幾個。 ### Collections (de)serialization 本次對大型集合做了顯著的改進(反序列化時為1.15x-1.5x,序列化時為1.5x-2.4x+)。我們可以在[dotnet/runtime #2259](https://github.com/dotnet/runtime/pull/2259)中更詳細地看到這些改進。 將.NET 5.0與.NET Core 3.1進行比較,對List(反序列化)的改進特別令人印象深刻。 這些變化將在高效能應用程式中非常有意義。 | Method | Mean | Error | StdDev | Median | Min | Max | Gen 0 | Gen 1 | Gen 2 | Allocated | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Deserialize before | 76.40 us | 0.392 us | 0.366 us | 76.37 us | 75.53 us | 76.87 us | 1.2169 | – | – | 8.25 KB | | After ~1.5x faster | 50.05 us | 0.251 us | 0.235 us | 49.94 us | 49.76 us | 50.43 us | 1.3922 | – | – | 8.62 KB | | Serialize before | 29.04 us | 0.213 us | 0.189 us | 29.00 us | 28.70 us | 29.34 us | 1.2620 | – | – | 8.07 KB | | After ~2.4x faster | 12.17 us | 0.205 us | 0.191 us | 12.15 us | 11.97 us | 12.55 us | 1.3187 | – | – | 8.34 KB | ### Property lookups — naming convention 使用JSON最常見的問題之一是[命名規範](https://en.wikipedia.org/wiki/Naming_convention_(programming)#Letter_case-separated_words)與.NET設計準則不匹配。JSON屬性通常是[camelCase](https://en.wikipedia.org/wiki/Camel_case), .NET屬性和欄位通常是PascalCase。我們使用的json序列化器負責在命名約定之間架橋。這不是免費的,至少對.NET Core 3.1來說不是。在.NET5中,這種成本現在可以忽略不計了。 .NET 5.0中大大改進了允許缺少屬性和不區分大小寫的程式碼。 在某些情況下[,速度快約1.75倍](https://github.com/dotnet/runtime/pull/35848)。 下面是一個簡單的4個屬性測試類的基準測試,它的屬性名為>7 bytes。 ``` 3.1 performance | 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 performance | 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 improvement 開發團隊在TechEmpower基準測試中花費了大量的精力來改進.NET的效能。使用TechEmpower JSON基準來驗證這些JsonSerializer改進是很有意義的。現在效能提高了~ 19%,一旦我們將條目更新到.NET5,這將提高.NET5在基準測試中的位置。這個版本的目標是與netty相比更具競爭力,netty是一種常見的Java web伺服器。 在[dotnet/runtime #37976](https://github.com/dotnet/runtime/pull/37976)中詳細介紹了這些更改和效能度量。這裡有兩套基準。第一個是使用團隊維護的[JsonSerializer效能基準測試](https://github.com/dotnet/performance/tree/master/src/benchmarks/micro/libraries/System.Text.Json/Serializer)來驗證效能。觀察到有~8%的改善。下一部分是關於技術授權的。它測量了滿足TechEmpower JSON基準測試要求的三種不同方法。SerializeWithCachedBufferAndWriter是我們在官方基準測試中使用的 | Method | Mean | Error | StdDev | Median | Min | Max | Gen 0 | Gen 1 | Gen 2 | Allocated | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | SerializeWithCachedBufferAndWriter (before) | 155.3 ns | 1.19 ns | 1.11 ns | 155.5 ns | 153.3 ns | 157.3 ns | 0.0038 | – | – | 24 B | | SerializeWithCachedBufferAndWriter (after) | 130.8 ns | 1.50 ns | 1.40 ns | 130.9 ns | 128.6 ns | 133.0 ns | 0.0037 | – | – | 24 B | 如果我們看一下Min列,我們可以做一些簡單的數學計算:153.3/128.6 = ~1.19。提高了19%。 ## Closing 本文對records和JsonSerializer有了一個更好的認識。它們只是.NET 5.0眾多改進中的兩個。[preivew 8](https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-8/?WT.mc_id=DOP-MVP-5003855)的文章涵蓋了更大的特性集,這為5.0的價值提供了更廣闊的視角。 正如我們所知道的,他們現在沒有在.NET 5.0中新增任何新特性。這些後期的預覽和RC的文章來涵蓋開發團隊已經建立的所有功能。當然大家可以在原文中進行留言,說一下在期望RC2中開發團隊這邊需要詳細介紹的特性。 原文:[https://devblogs.microsoft.com/dotnet/announcing-net-5-0-rc-1/](https://devblogs.microsoft.com/dotnet/announcing-net-5-0-rc-1/?WT.mc_id=DOP-MVP-5003855)