【翻譯】.NET 5 RC1釋出

HueiFeng發表於2020-09-15

9月14日,.NET5釋出了(Release Candidate)RC1版本,RC的意思是指我們可以進行使用,並且RC版本得到了支援,該版本很接近.NET5.0的最終版本,也是11月正式版本之前兩個RC版本中的其中一個。目前,開發團隊正在尋找在.NET5釋出之前剩餘的bug,當然他們也希望我們的反饋以幫助他們順利的完成.NET5的開發計劃。

開發團隊在今天還發布了ASP.NET CoreEF Core的RC1版本。

現在我們可以進行下載用於Windows、macOS和Linux的.NET5

如果要使用.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#9F#5.0

下面還有他們最近釋出的一些有關於.NET5.0新功能的文章,大家可以閱讀一下:

其實就像在.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 。 結果,初始化空的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,則應檢視遷移指南。 該指南闡明瞭這兩個API之間的關係。 System.Text.Json旨在涵蓋與Newtonsoft.Json相同的許多場景,但並不旨在替代流行的JSON庫或與流行的JSON庫實現功能對等。 我們嘗試在效能和可用性之間保持平衡,並在設計選擇中偏向效能。

HttpClient extension methods

JsonSerializer擴充套件方法現在在HttpClient上公開,並且極大地簡化了同時使用這兩個api。這些擴充套件方法消除了複雜性,併為您處理各種場景,包括處理內容流和驗證內容媒體型別。Steve Gordon很好地解釋了使用帶有System.Net.Http.Json的HttpClient傳送和接收JSON的好處

下面的示例使用新的GetFromJsonAsync()擴充套件方法將天氣預報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<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()擴充套件方法。在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開發證照

然後可以執行前面的示例。

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/

相關文章