9月14日,.NET5釋出了(Release Candidate)RC1版本,RC的意思是指我們可以進行使用,並且RC版本得到了支援,該版本很接近.NET5.0的最終版本,也是11月正式版本之前兩個RC版本中的其中一個。目前,開發團隊正在尋找在.NET5釋出之前剩餘的bug,當然他們也希望我們的反饋以幫助他們順利的完成.NET5的開發計劃。
開發團隊在今天還發布了ASP.NET Core和EF Core的RC1版本。
現在我們可以進行下載用於Windows、macOS和Linux的.NET5
- Installers and binaries
- Container images
- Snap installer
- Release notes
- Known issues
- GitHub issue tracker
如果要使用.NET5,我們需要使用最新的Visual Studio預覽版(包括Visual Studio for Mac)
在.NET5中有許多的改進,特別是對單檔案可執行應用程式、更小的容器映像、更強大的JsonSerializer api、BCL nullable reference type annotated、新target framework names,以及對Windows ARM64的支援。在.NET庫中,GC和JIT的效能都得到了極大的提升,ARM64是效能優化的重點,它為我們帶來了更好的吞吐量和更小的二進位制檔案。.NET5.0包含了新的語言版本,C#9和F#5.0。
下面還有他們最近釋出的一些有關於.NET5.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
其實就像在.NET5 Preview8中一樣,在本章還是像上一章一樣選擇了一些特性來進行深入的研究介紹,在本章中將深入的討論C#9中新特性records
和System.Text.Json.JsonSerializer
,它們是獨立的特性,但也是很好的一個組合,特別是在我們花費一些時間去為反序列化的JSON物件設計POCO型別時。
C# 9 — Records
Records
可能是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<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結束。該模式並不是新的命名模式的規範,這樣命名只是為了我們在程式碼片段中有相同型別的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<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
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<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為空。
為了進一步說明這一點,將不編譯以下內容。author.Name 初始化為null,然後不能更改,因為屬性是不可變的。
Author author = new(null, null);
author.Name = "Colin Meloy";
下面啟動可空性
<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' 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<Book> Books)
{
public string? Website {get; init;}
public string? Genre {get; init;}
public List<Author>? 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<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可以為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
我們將過渡到談論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,則應檢視遷移指南。 該指南闡明瞭這兩個API之間的關係。 System.Text.Json旨在涵蓋與Newtonsoft.Json相同的許多場景,但並不旨在替代流行的JSON庫或與流行的JSON庫實現功能對等。 我們嘗試在效能和可用性之間保持平衡,並在設計選擇中偏向效能。
HttpClient extension methods
JsonSerializer擴充套件方法現在在HttpClient上公開,並且極大地簡化了同時使用這兩個api。這些擴充套件方法消除了複雜性,併為您處理各種場景,包括處理內容流和驗證內容媒體型別。Steve Gordon很好地解釋了使用帶有System.Net.Http.Json的HttpClient傳送和接收JSON的好處。
下面的示例使用新的GetFromJsonAsync
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的頂級程式和record,以及新的GetFromJsonAsync
大家可以在自己的機器上試試。下面的.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開發證照。
然後可以執行前面的示例。
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<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一起使用的建構函式,對於類,如果只有一個建構函式,那麼屬性就不是必須的,與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>(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! }
Improved Dictionary<K,V> support
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
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<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"}
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<Employee> { 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<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;
}
Performance
在.NET 5.0中,JsonSerializer的效能得到了顯著改善。 Stephen Toub在.NET 5中的Performance Improvements中涵蓋了JsonSerializer的一些改進。 我會在這裡再介紹幾個。
Collections (de)serialization
本次對大型集合做了顯著的改進(反序列化時為1.15x-1.5x,序列化時為1.5x-2.4x+)。我們可以在dotnet/runtime #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最常見的問題之一是命名規範與.NET設計準則不匹配。JSON屬性通常是camelCase, .NET屬性和欄位通常是PascalCase。我們使用的json序列化器負責在命名約定之間架橋。這不是免費的,至少對.NET Core 3.1來說不是。在.NET5中,這種成本現在可以忽略不計了。
.NET 5.0中大大改進了允許缺少屬性和不區分大小寫的程式碼。 在某些情況下,速度快約1.75倍。
下面是一個簡單的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中詳細介紹了這些更改和效能度量。這裡有兩套基準。第一個是使用團隊維護的JsonSerializer效能基準測試來驗證效能。觀察到有~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的文章涵蓋了更大的特性集,這為5.0的價值提供了更廣闊的視角。
正如我們所知道的,他們現在沒有在.NET 5.0中新增任何新特性。這些後期的預覽和RC的文章來涵蓋開發團隊已經建立的所有功能。當然大家可以在原文中進行留言,說一下在期望RC2中開發團隊這邊需要詳細介紹的特性。
原文:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-rc-1/