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/