Liam W
封面

.NET 6 预览版 7 发布,最后一个预览版

作者
王亮·发表于 2 年前

原文:https://bit.ly/2VJxjxQ
作者:Richard
翻译:精致码农-王亮
说明:文中有大量的超链接,这些链接在公众号文章中被自动剔除,一部分包含超链接列表的小段落被我删减了,如果你对此感兴趣,请参考阅读原文。

我们很高兴地发布了 .NET 6 预览版 7。这是我们进入(两个)候选发布版(RC)之前的最后一个预览版。在我们放慢发布速度之前,团队一直在萤窗雪案,以完成最后一组功能。在这个版本中,你将看到各功能的最后一次抛光,一次到位地整合整个版本的大型功能。从此时起,团队将专注于使所有的功能达到统一的(高)质量,以便 .NET 6 为你的生产工作做好准备。

关于生产工作的话题,值得提醒大家的是,.NET 官网[1]和 Bing.com 从预览版 1 开始就一直运行在 .NET 6 上。我们正在与不同的团队(微软和其他公司)商谈有关进入生产的 .NET 6 RC 版本。如果你对此感兴趣,并希望得到相关的指导,请联系 dotnet@microsoft.com。我们始终很乐意与早期采用者交流。

你可以在这下载[2] Linux、macOS 和 Windows 的 .NET 6 预览版 7。

- 安装程序和二进制文件
  https://dotnet.microsoft.com/download/dotnet/6.0
- 容器镜像
  https://hub.docker.com/_/microsoft-dotnet
- Linux 包
  https://github.com/dotnet/core/blob/main/release-notes/6.0/install-linux.md
- 发布说明
  https://github.com/dotnet/core/blob/main/release-notes/6.0/README.md
- API 差异
  https://github.com/dotnet/core/tree/main/release-notes/6.0/preview/api-diff/preview7
- 已知问题
  https://github.com/dotnet/core/blob/main/release-notes/6.0/known-issues.md
- GitHub issue 跟踪
  https://github.com/dotnet/core/issues/6554

请参阅 .NET MAUI[3] 和 ASP.NET Core[4],了解更多关于客户端和 Web 应用场景的新内容。

.NET 6 预览版 7 已经在 Visual Studio 2022 预览版 3 中测试通过并得到支持。Visual Studio 2022 使你能够利用为 .NET 6 开发的 Visual Studio 工具,如 .NET MAUI 的开发、C# 应用程序的 Hot Reload、WebForms 的 Web Live,以及 IDE 体验中的其他性能改进。Visual Studio Code 也支持 .NET 6。

请查看我们新的对话帖[5],了解工程师之间关于最新的 .NET 功能的深入讨论。我们还发表了关于 C# 10 中的字符串插值和 .NET 6 中的预览功能 - 泛型 Math[6]

.NET SDK:现代化的 C# 项目模板

我们更新了 .NET SDK 的模板,以使用最新的 C# 语言特性和模式。我们已经有一段时间没有在新的语言特性方面重新审视这些模板了。现在是时候了,我们将确保模板在未来使用新的功能。

以下是新模板中使用的语言特性:

  • 顶层语句
  • async Main
  • 全局 using 指令(通过 SDK 驱动的默认值)
  • File-scoped 命名空间
  • 目标类型 new 表达式
  • 可空(Nullable)引用类型

你可能会问,为什么我们要通过模板启用某些功能,而不是在项目以 .NET 6 为 Target 时默认启用这些功能。尽管我们可以要求你在升级应用程序到新版本的 .NET 时做一些工作,作为改善平台默认行为的交换条件,这使我们能够改进产品,而不会使项目文件随着时间的推移而变得复杂。然而,有些功能对于这种模式来说可能是相当具有破坏性的,比如可空(Nullable)的引用类型。无论是在什么时候,我们都不想把这些功能与升级体验联系在一起,而是想把这个选择权留给你。模板是一个风险更低的支点,在那里我们能够为新的代码设置新的“好的默认模型”,而不会产生那么多下游的后果。通过项目模板启用这些功能,我们得到了两全其美的结果:新代码开始时启用了这些功能,但现有的代码在你升级时不会受到影响。

控制台模板

控制台模板变化最大,通过顶层语句和全局引用指令,它现在是一个单行代码:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

而以前的同一模板的 .NET 5 版本是这样的:

using System;

namespace Company.ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

控制台模板的项目文件也发生了变化,启用了可空(Nullable)引用类型的功能,例如:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

其他模板也可以实现可空(Nullable)引用类型、隐式全局引用和 File-scoped 命名空间,包括 ASP.NET Core 和 类库。

ASP.NET Web 模板

Web 模板也同样减少了代码行数,使用同样的功能:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", () => "Hello World!");

app.Run();

ASP.NET MVC 模板

MVC 模板的结构也类似。在这种情况下,我们将 Program.csStartup.cs 合并为一个文件(Program.cs),形成了进一步的简化:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

模板兼容性

关于使用新模板的兼容性问题,请参见以下内容:

  • 模板中的 C# 代码不被早期的 .NET 版本所支持[7]
  • 隐式命名空间引入[8]

类库:反射 API 的可空性信息

可空引用类型[9]是编写可靠代码的一个重要特征。它在编写代码时非常有用,但在检查代码时却没有(直到现在)。新的反射 API[10] 使你能够确定一个给定方法的参数和返回值的可空性。这些新的 API 对于基于反射的工具和序列化来说是至关重要的,比如说:

就上下文而言,我们在 .NET 5 中为 .NET 库添加了可空标注[11](在.NET 6 中完成),并且正在为 ASP.NET Core 的这个版本做同样的工作。我们也看到开发者在他们的项目中采用了可空性(Nullability)[12]

可空性(Nullability)信息存在于使用自定义属性的元数据中[13]。原则上,任何人都已经可以读取自定义属性,然而,这并不理想,因为编码的消耗非同小可。

下面的例子演示了在几个不同的场景中使用新的 API。

获取顶层的可空性信息

想象一下,你正在实现一个序列化器。使用这些新的 API,序列化器可以检查一个给定的属性是否可以被设置为 null

private NullabilityInfoContext _nullabilityContext = new NullabilityInfoContext();

private void DeserializePropertyValue(PropertyInfo p, object instance, object? value)
{
    if (value is null)
    {
        var nullabilityInfo = _nullabilityContext.Create(p);
        if (nullabilityInfo.WriteState is not NullabilityState.Nullable)
        {
            throw new MySerializerException(
quot;Property '
{p.GetType().Name}.{p.Name}'' cannot be set to null."
); } } p.SetValue(instance, value); }

获取嵌套的可空性信息

可空性对可以持有其他对象的对象有特殊处理,比如数组和元组。例如,你可以指定一个数组对象(作为一个变量,或者作为一个类型成员签名的一部分)必须是非空的,但是元素可以是空的,或者相反。这种额外的特殊性是可以通过新的反射 API 来检查的,例如:

class Data
{
    public string?[] ArrayField;
    public (string?, object) TupleField;
}
private void Print()
{
    Type type = typeof(Data);
    FieldInfo arrayField = type.GetField("ArrayField");
    FieldInfo tupleField = type.GetField("TupleField");

    NullabilityInfoContext context = new ();

    NullabilityInfo arrayInfo = context.Create(arrayField);
    Console.WriteLine(arrayInfo.ReadState);        // NotNull
    Console.WriteLine(arrayInfo.Element.State);    // Nullable

    NullabilityInfo tupleInfo = context.Create(tupleField);
    Console.WriteLine(tupleInfo.ReadState);                      // NotNull
    Console.WriteLine(tupleInfo.GenericTypeArguments [0].State); // Nullable
    Console.WriteLine(tupleInfo.GenericTypeArguments [1].State); // NotNull
}

类库:ZipFile 遵循 Unix 文件权限

System.IO.Compression.ZipFile 类现在可以在创建过程中捕获 Unix 文件权限,并在类似 Unix 的操作系统上提取压缩文件时设置文件权限。这一变化允许可执行文件在压缩包中被循环使用,这意味着你不再需要修改文件权限来使文件在解压缩包后可执行。它也同样遵循 usergroupother 读/写权限。

如果一个压缩包不包含文件权限(因为它是在 Windows 上创建的,或者使用了一个没有捕获权限的工具,比如早期的 .NET 版本),那么解压缩的文件就会得到默认的文件权限,就像其他新创建的文件一样。

Unix 的文件权限也适用于其他压缩工具,包括:

  • Info-ZIP
  • 7-Zip

早期 .NET 7 功能预览:泛型 Math

对于 .NET 6,我们已经建立了将 API 标记为“预览”的能力[14]。这种新方法将使我们能够在多个主要版本中提供和发展预览功能。为了使用预览 API,项目需要明确选择使用的预览功能。如果你在没有明确选择的情况下使用预览功能,从 .NET 6 RC1 开始,你会看到带有可操作信息的构建错误。预览功能预计将在以后的版本中发生变化,可能会有破坏性的变化。这就是为什么他们要选择加入。

我们在 .NET 6 中预览的这些功能之一是静态抽象接口成员。这些允许你在接口中定义静态的抽象方法(包括操作符)。例如,现在可以实现代数泛型方法。对于一些人来说,这个功能将是我们今年交付的绝对突出的改进。这也许是自 Span<T> 以来最重要的新类型系统特性。

下面的例子是一个 IEnumerable<T>,由于 T 被限制为 INumber<T>,可能是一个 INumber<int>,所以能够对所有的数值进行求和。

public static T Sum<T>(IEnumerable<T> values)
    where T : INumber<T>
{
    T result = T.Zero;

    foreach (var value in values)
    {
        result += value;
    }

    return result;
}

这是因为 INumber<T> 定义了各种(静态)操作符重载,必须由接口实现者来满足。IAdditionOperators 也许是最容易理解的新接口,INumber<T> 本身就是派生自这个接口。

这都是由一个新的功能提供的,它允许在接口中声明静态抽象成员。这使得接口可以公开运算符和其他静态方法,比如 ParseCreate,并且这些方法可以由派生类型实现。更多细节请参见我们的相关博文[15]

所有提到的功能都是 .NET 6 的预览版,不支持在生产中使用。我们将感谢您在使用中提供反馈。我们打算在 .NET 7 中继续发展和改进泛型 Math 功能以及支持它们的运行时和 C# 功能。我们希望对当前的体验进行突破性的改变,这也是为什么新的 API 被标记为“预览”的部分原因。

类库:NativeMemory API

我们增加了新的本地内存分配 API[16],通过 System.Runtime.InteropServices.NativeMemory 公开。这些 API 相当于 C 语言中的 mallocfreerealloccalloc API,还包括用于进行对齐分配的 API。

你可能想知道如何看待这些 API。首先,它们是低级别的 API,是为低级别的代码和算法准备的。应用程序开发人员很少会用到这些。另一种思考这些 API 的方式类似于平台内部的 API,它们是用于 CPU 指令的低级别 .NET API。这些 API 是类似的,但它是为内存相关的操作暴露的低级别的 API。

类库:System.Text.Json 序列化通知

System.Text.Json 序列化器现在将通知作为(反)序列化操作的一部分公开。它们对于默认值和验证非常有用。要使用它们,请在 System.Text.Json.Serialization 命名空间中实现一个或多个接口 IJsonOnDeserializedIJsonOnDeserializingIJsonOnSerialized 或 IJsonOnSerializing`。

这里有一个例子,在 JsonSerializer.Serialize()JsonSerializer.Deserialize() 中都进行验证,以确保 FirstName 属性不是 null

public class Person : IJsonOnDeserialized, IJsonOnSerializing
{
    public string FirstName{ get; set; }

    void IJsonOnDeserialized.OnDeserialized() => Validate(); // Call after deserialization
    void IJsonOnSerializing.OnSerializing() => Validate(); // Call before serialization

    private void Validate()
    {
        if (FirstName is null)
        {
            throw new InvalidOperationException("The 'FirstName' property cannot be 'null'.");
        }
    }
}

以前,你需要实现一个自定义转换器来实现这一功能。

类库:System.Text.Json 序列化属性排序

我们使用 System.Text.Json.Serialization.JsonPropertyOrderAttribute 特性增加了控制属性序列化顺序的能力,用一个整数指定了顺序,较小的整数先被序列化;没有该特性的属性有一个默认的排序值 0

这里有一个例子,指定 JSON 应该按照 Id, City, FirstName, LastName 的顺序进行序列化:

public class Person
{
    public string City { get; set; } // No order defined (has the default ordering value of 0)

    [JsonPropertyOrder(1)] // Serialize after other properties that have default ordering
    public string FirstName { get; set; }

    [JsonPropertyOrder(2)] // Serialize after FirstName
    public string LastName { get; set; }

    [JsonPropertyOrder(-1)] // Serialize before other properties that have default ordering
    public int Id { get; set; }
}

以前,序列化顺序是由反射顺序决定的,而反射顺序既不是确定的,也不会导致特定的预期顺序。

类库:System.Text.Json.Utf8JsonWriter

在用 Utf8JsonWriter 编写 JSON payloads 时,有时你需要嵌入“原始”JSON。

比如:

  • 我有一个设计好的字节序列,如下例所示。
  • 我有一个 blob,我认为它代表 JSON 内容,我想把它包起来,我需要确保包和它的内部保持良好的格式。
JsonWriterOptions writerOptions = new() { WriteIndented = true, };

using MemoryStream ms = new();
using UtfJsonWriter writer = new(ms, writerOptions);

writer.WriteStartObject();
writer.WriteString("dataType", "CalculationResults");

writer.WriteStartArray("data");

foreach (CalculationResult result in results)
{
    writer.WriteStartObject();
    writer.WriteString("measurement", result.Measurement);

    writer.WritePropertyName("value");
    // Write raw JSON numeric value using FormatNumberValue (not defined in the example)
    byte[] formattedValue = FormatNumberValue(result.Value);
    writer.WriteRawValue(formattedValue, skipValidation: true);

    writer.WriteEndObject();
}

writer.WriteEndArray();
writer.WriteEndObject();

以下是对上述代码–特别是FormatNumberValue–的描述。为了提高性能,System.Text.Json 在数字为整数时省略了小数点/值,如 1.0。其理由是,写的字节数越少越好,有利于提高性能。在某些情况下,保留小数点可能很重要,因为消费者将没有小数点的数字视为整数,否则视为浮点数。这种新的“原始值”模型允许你在任何需要的地方拥有这种程度的控制。

类库:JsonSerializer 同步流重载

我们为 JsonSerializer 添加了新的同步 API[17],用于将 JSON 数据序列化和反序列化到一个流。你可以在下面的例子中看到这个演示。

using MemoryStream ms = GetMyStream();
MyPoco poco = JsonSerializer.Deserialize<MyPoco>(ms);

这些新的同步 API 包括与新的 System.Text.Json source generator[18] 兼容和可用的重载,通过接受 JsonTypeInfo<T>JsonSerializerContext 实例。

类库:System.Diagnostics Propagators

在过去的几年里,我们一直在改进对 OpenTelemetry[19] 的支持。实现该支持的一个关键点是确保所有需要参与遥测生产的组件以正确的格式输出到网络头。要做到这一点真的很难,特别是随着 OpenTelemetry 规范的变化。OpenTelemetry 定义了传播(propagation)[20]的概念来帮助解决这种情况。我们正在采用传播的方式来实现头的定制的一般模型。

关于更广泛的概念背景:

  • OpenTelemetry 规范 - 分布式跟踪数据结构的内存表示。
  • OpenTelemetry Span - 追踪构建块,在 .NET 中由 System.Diagnostics.Activity 表示。
  • W3C TraceContext - 关于如何通过众所周知的 HTTP 头传播这些分布式跟踪数据结构的规范。

下面的代码演示了使用传播的一般方法:

DistributedContextPropagator propagator = DistributedContextPropagator.Current;
propagator.Inject(activity, carrier, (object theCarrier, string fieldName, string value) =>
{
   // Extract the context from the activity then inject it to the carrier.
});

你也可以选择使用不同的传播器(propagator):

// Set the current propagation behavior to not transmit any distributed context information in outbound network messages.
DistributedContextPropagator.Current = DistributedContextPropagator.CreateNoOutputPropagator();

DistributedContextPropagator 抽象类决定了分布式上下文信息在网络传输时是否以及如何被编码和解码。编码可以通过任何支持字符串键/值对的网络协议进行传输。DistributedContextPropagator 以字符串键/值对的形式向载体注入数值并从载体中提取数值。通过添加对传播者的支持,我们实现了两件事。

  • 你不再需要使用 W3C 的 TraceContext 头文件。你可以编写一个自定义的传播器(甚至用你自己的头文件名称),而不需要 HttpClient、ASP.NET Core 等库对这种自定义格式有预先的了解。
  • 如果你实现了一个带有自定义传输的库(如消息队列),只要你支持发送和接收文本映射(如 Dictionary<string, string>),你现在可以支持各种格式。

大多数应用程序代码不需要直接使用这个功能,然而,如果你使用 OpenTelemetry,你很可能会在调用栈中看到它。一些库的代码如果关心跟踪和因果关系,可能会需要使用这个模型。

类库:加密操作调用模式简化

.NET 的加密和解密部件是围绕着流设计的,没有真正的概念来定义什么时候有效载荷已经在内存中(already in memory)。SymmetricAlgorithm 上新的 Encrypt-Decrypt- 方法加速了 already in memory 的进展,目的是为调用者和代码审查者提供清晰的信息。此外,它们还支持从 span 中读取和写入。

新的简化方法为使用加密 API 提供了一个直接的方法:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;

        return aes.DecryptCbc(ciphertext, iv);
    }
}

在新的 Encrypt-Decrypt- 方法中,只使用 SymmetricAlgorithm 实例中的 Key 属性。新的 DecryptCbc 方法支持选择填充算法,但是 PKCS#7 经常与 CBC 一起使用,所以它是一个默认参数。如果你喜欢这种清晰的感觉,就指定它吧:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;

        return aes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7);
    }
}

你可以看到,现有的模式–使用 .NET 5–明显需要更多的管道来实现同样的结果:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;
        aes.IV = iv;

        // These are the defaults, but let's set them anyways.
        aes.Padding = PaddingMode.PKCS7;
        aes.Mode = CipherMode.CBC;

        using (MemoryStream destination = new MemoryStream())
        using (ICryptoTransform transform = aes.CreateDecryptor())
        using (CryptoStream cryptoStream = new CryptoStream(destination, transform, CryptoStreamMode.Write))
        {
            cryptoStream.Write(ciphertext, 0, ciphertext.Length);
            cryptoStream.FlushFinalBlock();
            return destination.ToArray();
        }
    }
}

运行时:支持所有平台和架构的 W^X

运行时现在有一种模式,它不创建或使用任何同时可写和可执行的内存页。所有可执行的内存都被映射为只读不执行。这项功能在该版本的早期仅在 macOS 上启用–针对 Apple Silicon。在 Apple Silicon 机器上,禁止同时进行可写和可执行的内存映射。

这一功能现在在所有其他平台上被启用并支持。在这些平台上,可执行代码的生成/修改是通过单独的读写内存映射完成的,这对 JIT 代码和运行时生成的辅助程序都是如此。这些映射是在与可执行代码地址不同的虚拟内存地址上创建的,并且只在进行写入时存在非常短暂的时间。例如,JIT 现在生成代码到一个从头开始的缓冲区,在整个方法被 jitted 后,使用一个内存拷贝函数调用将其复制到可执行内存中。而可写映射的寿命只跨越了内存拷贝的时间。

这个新功能可以通过设置环境变量 DOTNET_EnableWriteXorExecute 为 1 来启用。这个功能在 .NET 6 中是可选的,因为它有一个启动时的退步(除了在 Apple Silicon 上)。在我们的 ASP.NET 基准测试中,当用 Ready To Run(R2R)编译时,退步了 ~10%。然而,在启用和未启用该功能的情况下,测得的稳态性能是一样的。对于启动性能并不重要的应用程序,我们建议启用该功能,因为它能提高安全性。我们打算作为 .NET 7 的一部分解决性能退步问题,届时默认启用该功能。

结束

我们认为,我们已经到了新功能和改进已经完成的发布点。为团队点赞!

我们继续期待你们的反馈。我们将把 .NET 6 的其余部分放在完善(功能和性能)和新功能中发现的错误上。在大多数情况下,功能改进需要等到 .NET 7。请分享你的任何反馈,我们将很高兴对其进行分类。

感谢所有为 .NET 6 做出贡献的人,使其成为另一个伟大的版本。

感谢你成为一名 .NET 开发者。

文中相关链接:

[1].https://dotnet.microsoft.com/
[2].https://dotnet.microsoft.com/download/dotnet/6.0
[3].https://devblogs.microsoft.com/dotnet/announcing-net-maui-preview-7/
[4].https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-7/
[5].https://devblogs.microsoft.com/dotnet/category/conversations/
[6].https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/
[7].https://docs.microsoft.com/dotnet/core/compatibility/sdk/6.0/csharp-template-code
[8].https://docs.microsoft.com/dotnet/core/compatibility/sdk/6.0/implicit-namespaces
[9].https://docs.microsoft.com/dotnet/csharp/nullable-references
[10].https://github.com/dotnet/runtime/issues/29723
[11].https://twitter.com/JeffHandley/status/1424846146850131968
[12].https://github.com/jellyfin/jellyfin/blob/c07e83fdf87e61f30e4cca4e458113ac315918ae/Directory.Build.props#L5
[13].https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
[14].https://github.com/dotnet/designs/blob/main/accepted/2021/preview-features/preview-features.md
[15].https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/
[16].https://github.com/dotnet/runtime/pull/54006
[17].https://github.com/dotnet/runtime/issues/1574
[18].https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/
[19].https://devblogs.microsoft.com/dotnet/opentelemetry-net-reaches-v1-0/
[20].https://opentelemetry.lightstep.com/core-concepts/context-propagation/