- A+
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/