【翻譯】.NET 5 RC1釋出
阿新 • • 發佈:2020-09-15
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)