【翻译】.NET 5 RC1发布

proginn468312

共 23990字,需浏览 48分钟

 ·

2020-09-18 13:28

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中新特性recordsSystem.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: {lrr1 is IEquatable} ");
WriteLine($"{nameof(LoginResourceClass)} implements IEquatable: {lrr1 is IEquatable}");
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: True
LoginResourceClass implements IEquatable: 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 Books)
{
public string Website {get; init;}
public string Genre {get; init;}
public List 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>ExeOutputType>
<TargetFramework>net5.0TargetFramework>
<LangVersion>previewLangVersion>
<Nullable>enableNullable>
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 Books)
{
public string? Website {get; init;}
public string? Genre {get; init;}
public List? 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 _books = new();

public List Books => _books;

public string? Website {get; init;}
public string? Genre {get; init;}
public List? 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(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(json, options);

Console.WriteLine(forecast.Date);
Console.WriteLine(forecast.TemperatureC);
Console.WriteLine(forecast.TemperatureF);
Console.WriteLine(forecast.Summary);

var roundTrippedJson = JsonSerializer.Serialize(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);
Console.WriteLine(forecastJson);
Forecast? forecastObj = JsonSerializer.Deserialize(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 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.Serializeint, string>>(numbers);

Console.WriteLine(json);

var dictionary = JsonSerializer.Deserializeint, 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(json, options);

Console.WriteLine(forecast.Date);
Console.WriteLine(forecast.TemperatureC);
Console.WriteLine(forecast.TemperatureF);
Console.WriteLine(forecast.Summary);

var roundTrippedJson = JsonSerializer.Serialize(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 { 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(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 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/


浏览 17
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报