2020-09-15

【翻译】.NET 5 RC1发布

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 runTest record equality -- lrr1 == lrr2 : TrueTest class equality -- lrc1 == lrc2 : FalsePrint lrr1 hash code -- lrr1.GetHashCode(): -542976961Print lrr2 hash code -- lrr2.GetHashCode(): -542976961Print lrc1 hash code -- lrc1.GetHashCode(): 54267293Print lrc2 hash code -- lrc2.GetHashCode(): 18643596LoginResourceRecord implements IEquatable<T>: TrueLoginResourceClass implements IEquatable<T>: FalsePrint 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 runBattery { 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: FalseProperty 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 webapirich@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-forecastrich@thundera ~ % cd weather-forecastrich@thundera weather-forecast % dotnet run9/9/2020 12:09:19 PM; 24C; Chilly9/10/2020 12:09:19 PM; 54C; Mild9/11/2020 12:09:19 PM; -2C; Hot9/12/2020 12:09:19 PM; 24C; Cool9/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 run9/6/2020 11:31:01 AM-131Scorching{"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 run9/6/2020 11:31:01 AM-131Scorching{"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(反序列化)的改进特别令人印象深刻。 这些变化将在高性能应用程序中非常有意义。

MethodMeanErrorStdDevMedianMinMaxGen 0Gen 1Gen 2Allocated
Deserialize before76.40 us0.392 us0.366 us76.37 us75.53 us76.87 us1.21698.25 KB
After ~1.5x faster50.05 us0.251 us0.235 us49.94 us49.76 us50.43 us1.39228.62 KB
Serialize before29.04 us0.213 us0.189 us29.00 us28.70 us29.34 us1.26208.07 KB
After ~2.4x faster12.17 us0.205 us0.191 us12.15 us11.97 us12.55 us1.31878.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是我们在官方基准测试中使用的

MethodMeanErrorStdDevMedianMinMaxGen 0Gen 1Gen 2Allocated
SerializeWithCachedBufferAndWriter (before)155.3 ns1.19 ns1.11 ns155.5 ns153.3 ns157.3 ns0.003824 B
SerializeWithCachedBufferAndWriter (after)130.8 ns1.50 ns1.40 ns130.9 ns128.6 ns133.0 ns0.003724 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/

【翻译】.NET 5 RC1发布
品牌化wish高效运营核心要素1淘网上线两个月连夺亚马逊多个美妆产品销量第一,不可思议!预警:智利罢工已致船公司临时改港,印度大罢工即将举行!避坑指南:亚马逊UPC、EAN等GTIN的那些事!广告出价高,却没有曝光点击?用好动态竞价飙升100倍+Shopee开店准备黄远dmm.adulteori为了积分,珍藏的资料,解密亚马逊业内黑科技如何操作。如何通过数据化选品,开发蓝海小类目产品?如何购买通用产品代码(UPC)亚马逊着眼于拉美扩张,开设阿根廷办事处!【供应商资源】-----泳池清洁刷(三角透明地刷)

No comments:

Post a Comment