.NET 5.0 RC1 发布,离正式版发布仅剩两个版本,与 netty 相比更具竞争力

原文:http://dwz.win/Qf8
作者:Richard
翻译:精致码农-王亮
说明:
1. 本译文并不是完全逐句翻译的,存在部分语句我实在不知道如何翻译或组织就根据个人理解用自己的话表述了。
2. 本文有不少超链接,由于微信公众号和头条平台外链会被剔除 URL 地址,所以原来本是超链接的内容会显示为纯文本,如果你需要这些信息你可以移步到我的知乎博客园阅读(搜索精致码农可找到我)。

今天我们发布了 .NET 5.0 Release Candidate 1 (RC1)。它是目前最接近 .NET 5.0 的一个版本,也是在 11 月正式发布之前的两个 RC 版本中的第一个 RC 版本。RC1 是一个“上线”版本,表示你可以在生产环境中使用它了。

与此同时,我们一直在寻找最终正式版发布之前应该被修复的任何关键错误报告。我们需要你的反馈来帮助我们一起跨越 .NET 5.0 正式发布这道胜利的终点线。

我们今天也发布了 ASP.NET Core 和 EF Core 的 RC1 版本。

你可以下载适用于 Windows、macOS 和 Linux 的 .NET 5.0 版本:

  • 安装程序和二进制包

  • 容器镜像

  • 快速安装程序

  • 发布说明

  • 已知问题

  • GitHub Issue 跟踪

你需要最新的预览版 Visual Studio (包括 Visual Studio for Mac) 才能使用 .NET 5.0。

.NET 5.0 有很多改进,特别是单个文件应用程序、更小的容器映像、更强大的 JsonSerializer API、完整的可空引用类型标注、新的目标 Framework 名称,以及对 Windows ARM64 的支持。在网络库、GC 和 JIT 中性能得到了极大的提高。我们花了很大的工作在 ARM64 的性能上,它有了更好的吞吐量和更小的二进制文件。.NET 5.0 包含了新的语言版本:C# 9.0 和 F# 5.0。

我们最近发布了一些关于 5.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

就像我在 .NET 5.0 预览 8 文中所做的一样,我选择了一些特性来进行更深入的介绍,并让你了解如何在实际使用中使用它们。这篇文章专门讨论 C# 9 中的 System.Text.Json.JsonSerializerrecords(记录)。它们是独立的特性,但也是很好的组合,特别是如果你花费大量时间为反序列化的 JSON 对象创建 POCO 类型。

C# 9 — 记录

记录(原文 Record)可能是 C# 9 中最重要的新特性。它们提供了广泛的特性集(对一种语言类型来说),其中一些需要 RC1 或更高版本(如 record.ToString())。

译注:为了阅读更通顺,对 Record 的翻译,本译文根据语境的情况,有的地方用的是“Record”,有的地方用的是“记录”。因为在一些语境下把“Record”翻译成“记录”容易产生数据记录的错误联想。

最简单的理解,记录是不可变类型。在特性方面,它们最接近元组(Tuple),可以将它们视为具有属性且不可变的自定义元组。在如今使用元组的多数情况下,记录可以比元组提供更好更多的功能和使用场景。

在使用 C# 时,如果你使用命名类型会使你得到最好的体验(相对于像元组这样的特性)。静态类型(static typing)是该语言的设计要点,记录使小型类型更容易使用,并在整个应用程序中可以保证类型安全。

记录是不可变数据类型

记录使你能够创建不可变的数据类型,这对于定义存储少量数据的类型非常有用。

下面是一个记录的例子,它存储登录用户信息。

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 只允许在对象构造期间给属性赋值,它是记录不变性所依赖的基石。任何类型都可以使用 init,正如你在前面的类定义中看到的那样,它并不局限于在记录中使用。

private set 看起来类似于 initprivate set 防止其他代码(类型以外的代码)改变数据。当类型(在构造之后)意外地改变属性时,init 将产生编译错误。private set 不能使数据不可变,因此当类型在构造后改变属性值时,不会生成任何编译错误或警告。

记录是特殊的类

正如我刚才提到的,LoginResource 的记录变体和类变体几乎是相同的。类定义是记录的一个语义相同的子集,记录提供了更多特殊的行为。

为了让我们的想法达成一致,如前所述,下面的比较是一个记录和一个使用 init 代替 set 修饰属性的类之间的区别。

有哪些共同点:

  • 构造函数

  • 不变性

  • 复制语义(记录本质是类)

有哪些不同点:

  • 记录相等是基于内容的,类相等是基于对象标识;

  • 记录提供了一个基于内容 GetHashCode() 实现;

  • 记录提供了一个IEquatable<T>的实现,它使用 GetHashCode() 唯一性作为行为机制,为记录提供基于内容的相等语义;

  • 记录重写(override)了 ToString(),打印的是记录的内容。

记录和(使用 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 结束,该模式并不是新的命名约定,这样命名只是为了在样本中有相同类型的记录和类变体,请不要这样命名你的类。

此代码的输出如下:

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

记录的语法

有多种用于声明记录的模式,用于满足不同场景的使用。在玩过每个模式之后,你开始会对每种模式的好处有一个感性的认识。你还将看到,它们不是不同的语法,而是选项的连续体(continuum of options)。

第一个模式是最简单的 —— 一行代码 —— 但是提供的灵活性最小,它适用于具有少量必需属性(必需属性,即初始化时必需给作为参数的属性传值)的记录。

以下用前面展示的 LoginResource 记录作为此模式的一个示例。就这么简单,一行代码就是整个定义:

public record LoginResource(string Username, string Password, bool RememberMe);

构造遵循带参数的构造函数的要求(包括允许使用可选参数):

var login = new LoginResource("Lion-O", "jaga", true);

如果你喜欢,也可以用 target typing:

LoginResource login = new("Lion-O", "jaga", true);

下面这个语法使所有属性都是可选的,为记录提供了一个隐式无参数构造函数。

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
};

不要认为记录只用于不可变数据。你可以置入公开可变属性,如下面的示例所示,该示例报告了关于电池的信息。ModelTotalCapacityAmpHours 属性是不可变的,而 RemainingCapacityPercentange 是可变的。

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 }

无损式记录修改

不变性提供了显著的好处,但是您很快就会发现需要对记录进行改变的情况。你怎么能在不放弃不变性的前提下做到这一点呢?with 表达式满足了这一需求。它支持根据相同类型的现有记录创建新记录。你可以指定你想要的不同的新值,并且从现有记录复制所有其他属性。

让我们把用户名转换成小写,这是用户名在我们假定的一个用户数据库中的存储方式。但是,为了进行诊断,需要使用原始用户名大小写。假设以前面示例中的代码为例,它可能像下面这样:

LoginResource login = new("Lion-O", "jaga", true);
LoginResource loginLowercased = lrr1 with {Username = login.Username.ToLowerInvariant()};

login 记录没有被更改,事实上这也是不允许的。转换只影响了 loginLowercased,除了将小写转换为 loginLowercased 之外,其它与 login 是相同的。

我们可以使用内置的 ToString() 检查 width 是否完成了预期的工作:

Console.WriteLine(login);
Console.WriteLine(loginLowercased);

此代码输出如下结果:

LoginResource { Username = Lion-O, Password = jaga, RememberMe = True }
LoginResource { Username = lion-o, Password = jaga, RememberMe = True }

我们可以进一步了解 with 是如何工作的,它将所有值从一条记录复制到另一条记录。这不是一个记录依赖于另一个记录的模型。事实上,with 操作完成后,两个记录之间就没有关系了,只在对记录的构建时有意义。这意味着对于引用类型,副本只是引用的副本;对于值类型,是复制值。

你可以在下面的代码中看到这种语义:

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

记录的实例

对记录进行扩展是很容易的。让我们假设一个新的 LastLoggedIn 属性,它可以直接添加到 LoginResource。那是个好的设想,记录不像传统的接口那样脆弱,除非你想让该新属性在创建时作为构造函数所必需的参数。

在这个案例中,现在我想使 LastLoggedIn 是必需的。想象一下,代码库非常大,把这个修改反应到所有创建 LoginResource 的地方工作量是巨大的。相反,我们将用这个新属性创建一个扩展 LoginResource 的新 Record。现有代码将在 LoginResource 方面工作,新代码将在新 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 设置为一个必需的属性,并利用这个机会添加了附加的且可选的属性,这些属性可能设置也可能没有设置值。通过扩展 LoginResource 记录,还定义了可选的 RememberMe 属性。

记录的构造辅助

其中一个不是很直观的模式是建模辅助(modeling 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)
};

在本例中,需要说明的是 weight 是本地变量,不可能在对象初始化器中访问 Kilograms 属性。也有必要将 GetPounds 定义为静态方法,因为不可能在对象初始化器中调用实例(它还未构造完成)方法。

记录和可空性

语法上,记录是具有可空性(Nullability)的对吗?既然记录是不可变的,那 null 从何而来呢?如果初始值就是 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 是 null(译者疑问:真的是编译时报错而不是运行时报错吗?期待大家亲测)。

为了更进一步说明这一点,下面的代码无法编译通过,因为 author.Name 初始值为 null,然后是不能更改的,因为属性是不可变的。

Author author = new(null, null);
author.Name = "Colin Meloy";

我要更新我的 project 文件,以启用可空性。

<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' \n
must contain a non-null value when exiting constructor. Consider declaring the property as \n
nullable. [/Users/rich/recordsnullability/recordsnullability.csproj]

我用我用可空修饰符更新了 Author 记录,这些可空修饰符描述了我打算如何使用该记录。

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 的警告,之前看到的 Author 的 null 构造。

/Users/rich/recordsnullability/Program.cs(5,21): warning CS8625: Cannot convert null literal \n
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 可以为空,编译器可以看到 RelatedAuthors 属性是在前面几行设置的,因此它知道 RelatedAuthors 引用是非空的。

但是,想象一下如果这个程序是这样的:

Author GetAuthor()
{return new Author("Karen Lord"){Website = "https://karenlord.wordpress.com/",RelatedAuthors = new()};
}Author lord = GetAuthor();

当类型构造在一个单独的方法中时,编译器不能智能地知道 RelatedAuthors 是非空的。在这种情况下,将需要以下两种模式之一:

lord.RelatedAuthors!.AddRange(

if (lord.RelatedAuthors is object)
{lord.RelatedAuthors.AddRange( ...
}

这是一个关于记录可空性的冗长演示,只是想说明它不会改变使用可空引用类型的任何体验。

另外,您可能已经注意到,我将 Author 记录上的 Books 属性改为一个初始化的 get-only 属性,而不是记录构造函数中的一个必需参数。这是因为 AuthorBooks 之间存在一种循环关系(译注:Author 含有List<Book>类型的导航属性,Book 也包含 Author 类型的导航属性)。不变性和循环引用可能会导致头痛。在本例中,这是可以的,只是意味着需要在 Book 对象之前创建所有 Author 对象。因此,不可能在 Author 构造中提供一组完全初始化好的 Book 对象作为 Author 构建的一部分,我们所能期待的最好结果就是一个空的 List<Book>。因此,初始化一个作为 Author 构建的一部分的空 List<Book> 似乎是最好的选择。没有规则规定所有这些属性都必须是 init 的形式,我(示例中)之所以这样做是为了示范。

我们将转移到 JSON 序列化的话题。这个带有循环引用的示例与稍后将在 JSON 对象图部分中的保存引用有关。JsonSerializer 支持循环引用的对象图,但不支持带有参数化构造函数的类型。你可以将 Author 对象序列化为 JSON,但不能将其反序列化为当前定义的 Author 对象。如果 Author 不是记录或者没有循环引用,那么序列化和反序列化都可以使用 JsonSerializer。

System.Text.Json

System.Text.Json 在 .NET 5.0 中得到了显著的改进,提高了性能和可靠性,并使熟悉 Newtonsoft.Json 的人更容易采用它。它还支持将 JSON 对象反序列化为记录,这是本文之前的文章介绍过的 C# 新特性。

如果你想将 System.Text.Json 作为 Newtonsoft.Json 的替代品,可以看这个 迁移指南,该指南阐明了这两者 API 之间的关系。System.Text.Json 旨在涵盖与 Newtonsoft.Json 相同的大多数场景,但是它并不是用来替代该流行的 Json 库的,也不是为了实现与流行的 Json 库相同的功能。我们试图在性能和可用性之间保持平衡,并在设计选择中偏向于性能。

HttpClient 扩展方法

JsonSerializer 扩展方法现在公开到 HttpClient 上了,极大地简化了同时使用这两个 API。这些扩展方法消除了复杂性,并为你处理各种场景,包括处理内容流和验证内容媒体类型。Steve Gordon 很好地解释了使用基于 System.Net.Http.Json 的 HttpClient 发送和接收 JSON 的好处。

下面的示例使用新的 GetFromJsonAsync<T>() 扩展方法将天气预报的 JSON 数据反序列化为 Forecast 记录。

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 的顶层程序和记录,以及新的 GetFromJsonAsync<T>() 扩展方法。如此近距离使用 foreachawait 可能会让你怀疑我们是否会添加对 JSON 对象流的支持。是的,在未来的版本中。

译注:上面作者所说的“近距离”我觉得意思是指反序列化时就近声明需要的记录类型,比单独创建 Model 类放在单独的文件中“近”许多。

你可以在你自己的机器上试试,下面的 .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 --trust,否则客户端和服务器之间的将不能正常握手通讯。如果有问题,请参见 Trust the ASP.NET Core HTTPS development certificate.。

然后你可以运行前面的例子:

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

改进了对不可变类型的支持

定义不可变类型有多种模式,记录只是最新的一种(比如下文示例中的一个 Struct 类型),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 一起使用的构造函数。对于类,如果只有一个构造函数,那么该特性就不是必需的,记录也是如此。

它的输出如下:

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"}

支持记录

JsonSerializer 对记录的支持几乎与我刚才对不可变类型的支持相同。这里我想展示的不同之处是将一个 JSON 对象反序列化为一个记录,该记录公开一个参数化的构造函数和一个可选的 init 属性。

下面是一个包含了该记录定义的程序:

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! }

改进了 Dictionary<K,V> 的支持

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

支持字段

JsonSerializer 现在支持字段,这个变化是由 @YohDeadfall 贡献的,感谢他!

你可以在下面的示例中看到它的样子,在 .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"}

保留 JSON 对象图中的引用

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 defaultDefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,// NEW: globally allow reading and writing numbers as JSON stringsNumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,// NEW: globally support preserving object references when (de)serializingReferenceHandler = ReferenceHandler.Preserve,IncludeFields = true, // NEW: globally include fields for (de)serializationWriteIndented = 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;
}

性能

JsonSerializer 的性能在 .NET 5.0 中得到了显著提高。Stephen Toub 在他的 .NET 5 的性能改进 一文中介绍了一些 JsonSerializer 的改进,我将在这里再介绍一些。

集合的(反)序列化

我们对大型集合做了显著的改进(反序列化时为 1.15x-1.5x,序列化时为 1.5x-2.4x+)。你可以在 dotnet/runtime #2259 中更详细地看到这些改进。

与 .NET 5.0 和 .NET Core 3.1 相比,List<int> (反)序列化的改进特别令人印象深刻,这些变化将在高性能应用程序中体现出来。

属性查找 —— 命名约定

使用 JSON 最常见的问题之一是命名约定与 .NET 设计准则不匹配。JSON 属性通常是 camelCase,.NET 属性和字段通常是 PascalCase。你使用的 JSON 序列化器负责在命名约定之间架桥。这不是轻易就能做到的,至少对 .NET Core 3.1 来说不是。但在 .NET 5.0 中,这种实现成本现在可以忽略不计了。

允许缺少属性和不区分大小写的代码在 .NET 5.0 中得到了极大的改进,在某些情况下它要快 1.75 倍。

下面是一个简单的四属性测试类的基准测试,它的属性名为大于 7 字节。

3.1 性能
|                            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 性能
|                            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 改进

译注:TechEmpower 是一家主要做基准测试的公司,它会定期提供各种 Web 应用程序框架性能指标的测试和比较,覆盖了许多的语言框架,包括 C#,Go,Python,Java,Ruby,PHP 等。测试基于云和物理硬件,测试的性能则包括纯文本响应、序列化 JSON 对象、单个/多个数据库查询、数据库更新、Fortunes 测试等等。

我们在 TechEmpower 基准测试中花费了大量的精力来改进 .NET 的性能。使用 TechEmpower JSON 基准来验证这些 JsonSerializer 改进是有意义的。现在性能提高了约 19%,一旦我们将条目更新到 .NET 5.0 将提高 .NET 在基准测试中的排行位置。这个版本的目标是与 netty 相比更具竞争力,netty 是常见的 Java Webserver。

在 dotnet/runtime #37976 中详细介绍了这些更改和性能度量。这里有两套基准,第一个是使用团队维护的 JsonSerializer 性能基准测试来验证性能。观察到有约 8% 的改善;第二个是 TechEmpower 的,它测量了满足 TechEmpower JSON 基准测试要求的三种不同方法。我们在官方基准测试中使用的是SerializeWithCachedBufferAndWriter

如果我们看一下 Min 列,我们可以做一些简单的数学计算:153.3/128.6 = ~1.19,有了 19% 的提升。

结束

我希望你喜欢本文对记录和 JsonSerializer 的深入介绍,它们只是 .NET 5.0 众多改进中的两个。这篇预览 8 的文章涵盖了更多的新特性,这为 5.0 的价值提供了更广阔的视角。

正如你所知道的,我们目前阶段没有在 .NET 5.0 中继续添加新特性了。我利用后面的预览和 RC 版本发布的文章来涵盖我们已经添加的所有功能的介绍。你希望我在 RC2 发布的博客文章中介绍哪些内容?我想从你们那知道我应该关注什么。

请在评论中分享你使用 RC1 的体验,感谢所有安装了 .NET 5.0 的人,我们感谢到目前为止我们收到的所有参与和反馈。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307751.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一个例子带你搞懂python作用域中的global、nonlocal和local

在编程中&#xff0c;只要接触过函数的&#xff0c;我相信都理解什么是全局变量和局部变量&#xff0c;概念比较简单&#xff0c;这里就不做解释了。在python中&#xff0c;用global语句就能将变量定义为全局变量&#xff0c;但是最近又发现有个nonlocal&#xff0c;一时搞不太…

初识ABP vNext(10):ABP设置管理

点击上方蓝字"小黑在哪里"关注我吧定义设置使用设置前言上一篇介绍了ABP模块化开发的基本步骤&#xff0c;完成了一个简单的文件上传功能。通常的模块都有一些自己的配置信息&#xff0c;比如上篇讲到的FileOptions类&#xff0c;其中配置了文件的上传目录&#xff0…

类加载机制(整个过程详解)

一:背景 类加载机制是在我们的真个java的运行阶段中的其中一个阶段。 二:什么是快乐星球(类加载机制) 我们编写的 Java 文件都是以.java 为后缀的文件&#xff0c;编译器会将我们编写的.java 的文件编译成.class 文件&#xff0c;简单来说类加载机制就是jvm从文件系统将一系…

leetcode860. 柠檬水找零

一:论语 这里的小不忍指的是一方面我们受到挫折而控制不住自己而大发脾气&#xff0c;还有一方面指的是我们的过于优柔寡断&#xff0c;对于自己或者他人的过失&#xff0c;不忍心加以责罚。 二:题目 三:上码 class Solution { public:bool lemonadeChange(vector<int&g…

跟我一起学.NetCore之Asp.NetCore启动流程浅析

前言一个Asp.NetCore项目&#xff0c;知道大概的启动流程是有必要的&#xff0c;比如后续遇见配置信息覆盖等相关问题时也大概知道是什么原因&#xff0c;了解原因之后&#xff0c;再去搜索引擎找答案&#xff0c;否则目标不明确&#xff0c;茫茫人海怎么会一下找到自己想要的&…

leetcode406. 根据身高重建队列

一&#xff1a;你不一定逆风翻盘&#xff0c;但请一定向阳而生 二&#xff1a;题目 三&#xff1a;上码 class Solution { public:/**解析题意:这个给出的people的数组中,我们需要根据其元素people[i] [hi,ki];然后根据其ki来表示大于hi身高的人的个数&#xff0c;来进行排序…

.NET Core 下使用 RabbitMQ

介绍RabbitMQ是一个开源的,基于AMQP(Advanced Message Queuing Protocol)协议的完整,可复用的企业级消息队列(Message Queue 一种应用程序与应用程序之间的一种通信方法)系统,RabbitMQ可以实现点对点,发布订阅等消息处理模式官网&#xff1a;https://www.rabbitmq.com/dotnet.h…

leetcode452. 用最少数量的箭引爆气球

一:论语 少些自我感动&#xff0c;多谢反思&#xff0c;时刻警惕自己是否在假装很努力&#xff0c;自己懂不懂 自己会不会 自己想要什么 只有自己 最清楚 二&#xff1a;题目 三:上码 class Solution { public:/**思路:1.这道题类似无重叠空间,我们先按每个气球的右边界升序…

leetcode435. 无重叠区间

一&#xff1a;论语 道不同 不相为谋 我们没有理由拿着自己的评判标准 去看待别人所经历的事情&#xff0c;重来就没有真正的感同身受&#xff0c;我们能做的就是尊重他人的看法 &#xff0c;保留自己的态度。 二&#xff1a;题目 三:上码 class Solution { public:/**思路:…

Swagger扩展为你添油加气

关注架构师高级俱乐部开启架构之路不定期福利发放哦~Leon读完需要4分钟速读仅需 2 分钟介绍一款Swagger扩展日常接口开发中都需要用到Swagger来生成接口文档并用 Swagger 自带支持的模拟请求进行测试&#xff0c;但是需要支持认证或者上传文件等操作需要自行去按接口进行开发才…

leetcode763. 划分字母区间

一&#xff1a;论语 保留自己的态度&#xff0c;尊重别人的看法&#xff0c;不强迫别人按照自己的意愿做事。 二:题目 三:上码 class Solution { public:vector<int> partitionLabels(string s) {/**思路:1.在这里我们选取的数据结构是map容器,我们只要下记录每个字母…

跟我一起学.NetCore之依赖注入

前言现阶段而言&#xff0c;依赖注入相关组件如果不会用一两个&#xff0c;感觉在Code的世界里肯定是落伍了&#xff0c;最起码得有工厂模式的思想&#xff0c;知道这样做的好处&#xff1b;提及到依赖注入&#xff0c;通常会关联出两个概念&#xff1a;Ioc(控制反转)和DI(依赖…

跟我一起学.NetCore之依赖注入作用域和对象释放

前言上一小节简单阐述了依赖注入及Asp.NetCore中自带依赖注入组件的常规用法&#xff0c;其中提到容器管控了自己创建对象的生命周期&#xff0c;包含了三种生命周期&#xff1a;Singleton、Scoped、Transient&#xff0c; 对于Singleton、Transient相对于Scoped来说比较好理解…

leetcode738. 单调递增的数字

一:芭比Q了 又掉一个粉 啊呜呜呜呜呜 如果作为一个领导者来说&#xff0c;首先就是要以身作则&#xff0c;自己都做不到 &#xff0c;那就没什么威信去要求手下人按照要求去做 二:题目 三:上码 class Solution { public:int monotoneIncreasingDigits(int n) {/**思路:1.这…

大揭秘| 我司项目组Gitlab Flow DevOps流程

长话短说&#xff0c;本文全景呈现我司项目组gitlab flow && devopsGit Flow定义了一个项目发布的分支模型&#xff0c;为管理具有预定发布周期的大型项目提供了一个健壮的框架。DevOps 强调的是团队通过自动化的工具协作和高效地沟通来完成软件的生命周期管理&#xf…

leetcode714.买卖股票的

一:题目 二&#xff1a;上码 class Solution { public:int maxProfit(vector<int>& prices, int fee) {/**思路:*/int ans 0;int minPrice prices[0];//最低时买入for(int i 1; i < prices.size(); i) {//低价买入minPrice min(minPrice,prices[i]);if(price…

C++ 学习之旅(1)——编译器Compiler

简单来说&#xff0c;由C代码文件生成可执行文件的过程如下&#xff1a; #mermaid-svg-GQamCVEXMVkYEemz {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GQamCVEXMVkYEemz .error-icon{fill:#552222;}#mermaid-svg-…

掌握Rabbitmq几个重要概念,从一条消息说起

RabbitMQ 是功能强大的开源消息代理。根据官网称&#xff1a;也是使用量最广泛的消息队列。就像他的口号“Messaging that just works”&#xff0c;开箱即用使用简单&#xff0c;支持多种消息传输协议&#xff08;AMQP、STOMP、MQTT&#xff09;。一个应用程序或者服务如何使用…

122. 买卖股票的时机

一&#xff1a;题目 二&#xff1a;上码 class Solution { public:int maxProfit(vector<int>& prices) {/**思路:1.这里用的是贪心算法&#xff08;我们每隔两天计算一次赚的钱 只要大于0的话 那就是赚的&#xff09;2.我们手里最多只能有一只股票,所以我们可以当…

C++ 学习之旅(2)——链接器Linker

每一个.cpp文件经过编译之后都会生成对应的.obj文件&#xff0c;然后通过链接器把它们进行链接&#xff0c;最后就可以生成.exe可执行文件了。 举个例子&#xff0c;假设我们有一个 Math.cpp 文件和 Log.cpp 文件&#xff1a; Math.cpp #include <iostream>void Log(c…