.NET 7预览版5发布——亮点说明

124 阅读9分钟

今天,我们发布了.NET 7预览版5。这个.NET 7预览版包括对Generic Math的改进,使API作者的生活更轻松;为ML.NET提供的新的文本分类API,为自然语言处理增加了最先进的深度学习技术;对源代码生成器的各种改进和RegexGenerator的新Roslyn分析器和修复器,以及在CodeGen、Observability、JSON序列化/反序列化和使用流方面的多个性能改进。

你可以下载.NET 7 Preview 5,适用于Windows、macOS和Linux:

.NET 7预览版5已经用Visual Studio 17.3 Preview 2进行了测试。如果你想用Visual Studio家族产品尝试.NET 7,我们建议你使用预览通道构建。如果你是在macOS上,我们建议使用最新的Visual Studio 2022 for Mac预览版。现在,让我们进入这个版本中的一些最新更新。

可观察性

可观察性的目标是帮助你在规模和技术复杂性增加时更好地了解你的应用程序的状态。

暴露高性能的ActivityEvent和ActivityLink标签枚举器方法

#68056

暴露的方法可用于性能关键的场景,在没有任何额外分配和快速项目访问的情况下枚举标签对象。

var tags = new List<KeyValuePair<string, object?>>()
{
    new KeyValuePair<string, object?>("tag1", "value1"),
    new KeyValuePair<string, object?>("tag2", "value2"),
};

ActivityLink link = new ActivityLink(default, new ActivityTagsCollection(tags));

foreach (ref readonly KeyValuePair<string, object?> tag in link.EnumerateTagObjects())
{
    // Consume the link tags without any extra allocations or value copying.
}            

ActivityEvent e = new ActivityEvent("SomeEvent", tags: new ActivityTagsCollection(tags));

foreach (ref readonly KeyValuePair<string, object?> tag in e.EnumerateTagObjects())
{
    // Consume the event's tags without any extra allocations or value copying.
} 

系统.文本.Json

多态性

#63747

System.Text.Json现在支持使用属性注解对多态类型层次进行序列化和反序列化。

[JsonDerivedType(typeof(Derived))]
public class Base
{
    public int X { get; set; }
}

public class Derived : Base
{
    public int Y { get; set; }
}

此配置使Base 的多态序列化成为可能,特别是当运行时类型为Derived

Base value = new Derived();
JsonSerializer.Serialize<Base>(value); // { "X" : 0, "Y" : 0 }

请注意,这并不能启用多态反序列化,因为有效载荷将被往返作为Base

Base value = JsonSerializer.Deserialize<Base>(@"{ ""X"" : 0, ""Y"" : 0 }");
value is Derived; // false

使用类型判别器

为了启用多态反序列化,用户需要为派生类指定一个类型判别器

[JsonDerivedType(typeof(Base), typeDiscriminator: "base")]
[JsonDerivedType(typeof(Derived), typeDiscriminator: "derived")]
public class Base
{
    public int X { get; set; }
}

public class Derived : Base
{
    public int Y { get; set; }
}

现在它将与类型鉴别器元数据一起发射JSON:

Base value = new Derived();
JsonSerializer.Serialize<Base>(value); // { "$type" : "derived", "X" : 0, "Y" : 0 }

可用于多态地反序列化值:

Base value = JsonSerializer.Deserialize<Base>(@"{ ""$type"" : ""derived"", ""X"" : 0, ""Y"" : 0 }");
value is Derived; // true

类型鉴别器的标识符也可以是整数,所以下面的形式是有效的:

[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }

JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }

Utf8JsonReader.CopyString

#54410

直到今天。 Utf8JsonReader.GetString()一直是用户可以消费解码后的JSON字符串的唯一方式。这将总是分配一个新的字符串,这可能不适合某些对性能敏感的应用程序。新加入的CopyString 方法允许将未转义的UTF-8或UTF-16字符串复制到用户拥有的缓冲区中。

int valueLength = reader.HasReadOnlySequence ? checked((int)ValueSequence.Length) : ValueSpan.Length;
char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);

ParseUnescapedString(source); // handle the unescaped JSON string
ArrayPool<char>.Shared.Return(buffer);

或者说,如果处理UTF-8是比较好的:

ReadOnlySpan<byte> source = stackalloc byte[0];
if (!reader.HasReadOnlySequence && !reader.ValueIsEscaped)
{
    source = reader.ValueSpan; // No need to copy to an intermediate buffer if value is span without escape sequences
}
else
{
    int valueLength = reader.HasReadOnlySequence ? checked((int)ValueSequence.Length) : ValueSpan.Length;
    Span<byte> buffer = valueLength <= 256 ? stackalloc byte[256] : new byte[valueLength];
    int bytesRead = reader.CopyString(buffer);
    source = buffer.Slice(0, bytesRead);
}

ParseUnescapedBytes(source);

源码生成的改进

增加了对IAsyncEnumerable<T> (#59268)、JsonDocument (#59954)和DateOnly/TimeOnly (#53539)类型的源生成支持。

比如说:

[JsonSerializable(typeof(typeof(MyPoco))]
public class MyContext : JsonSerializerContext {}

public class MyPoco
{
    // Use of IAsyncEnumerable that previously resulted 
    // in JsonSerializer.Serialize() throwing NotSupportedException 
    public IAsyncEnumerable<int> Data { get; set; } 
}

// It now works and no longer throws NotSupportedException
JsonSerializer.Serialize(new MyPoco { Data = ... }, MyContext.MyPoco); 

系统.IO.Stream

ReadExactly和ReadAtLeast

#16598

使用Stream.Read() ,最常见的错误之一是:Read() 可能返回的数据比Stream 中的数据少,也比传入的缓冲区少。而即使是意识到这一点的程序员,每次想从Stream ,都要写同样的循环,也是很烦人的。

为了帮助这种情况,我们在基础System.IO.Stream 类中增加了新的方法:

namespace System.IO;

public partial class Stream
{
    public void ReadExactly(Span<byte> buffer);
    public void ReadExactly(byte[] buffer, int offset, int count);

    public ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default);
    public ValueTask ReadExactlyAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default);

    public int ReadAtLeast(Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true);
    public ValueTask<int> ReadAtLeastAsync(Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true, CancellationToken cancellationToken = default);
}

新的ReadExactly 方法可以保证准确地读取所要求的字节数。如果在所请求的字节被读取之前Stream就结束了,就会抛出一个EndOfStreamException

using FileStream f = File.Open("readme.md");
byte[] buffer = new byte[100];

f.ReadExactly(buffer); // guaranteed to read 100 bytes from the file

新的ReadAtLeast 方法将至少读取请求的字节数。如果有更多的数据可用,它可以读取更多的数据,最多是缓冲区的大小。如果Stream在读取所要求的字节之前结束,会抛出一个EndOfStreamException (在高级情况下,当你想获得ReadAtLest 的好处,但你也想自己处理Stream结束的情况,你可以选择不抛出这个异常)。

using FileStream f = File.Open("readme.md");
byte[] buffer = new byte[100];

int bytesRead = f.ReadAtLeast(buffer, 10);
// 10 <= bytesRead <= 100

新的Roslyn分析器和RegexGenerator的修复器

#69872

在《.NET 7中的正则表达式改进》中,Stephen Toub描述了新的RegexGenerator源码生成器,它允许你在编译时静态地生成正则表达式,从而带来更好的性能。要利用这一点,首先你必须在你的代码中找到可以使用它的地方,然后对每个代码进行修改。这听起来像是Roslyn分析器和修正器的完美工作,所以我们在Preview 5中添加了一个。

分析器

新的分析器包含在.NET 7中,它将搜索那些可以转换为使用RegexGenerator源生成器的Regex 的使用。分析器将检测对Regex 构造函数的使用,以及对符合以下条件的Regex 静态方法的使用。

  • 提供的参数在编译时有一个已知值。源码生成器的输出依赖于这些值,所以它们在编译时必须是已知的。
  • 它们是针对.NET 7的应用程序的一部分。新的分析器在.NET 7目标包内,只有针对.NET 7的应用程序才有资格使用这个分析器。
  • LangVersion (了解更多)高于10 。目前,重词源生成器需要将LangVersion 设置为preview

下面是新的分析器在Visual Studio中的运行情况:

image.png

代码修正器

代码修复器也包含在.NET 7中,它做两件事。首先,它建议一个RegexGenerator源码生成器方法,并给你一个覆盖默认名称的选项。然后,它用对新方法的调用来替换原来的代码。

下面是新的代码修复器在Visual Studio中的应用。

image.png

通用数学

在.NET 6中,我们预览了一项名为 "通用数学 "的功能,它允许.NET开发者在通用代码中利用静态API,包括运算符。这项功能将直接有利于API作者,他们可以简化他们的代码库。其他开发者将间接受益,因为他们使用的API将开始支持更多的类型,而不需要每一个数字类型都得到明确的支持。

在.NET 7中,我们对实现进行了改进,并对社区的反馈做出了回应。关于这些变化和可用的API的更多信息,请看我们的通用数学具体公告

调用成员时System.Reflection的性能改进

#67917

当对同一个成员进行多次调用时,使用反射来调用一个成员(无论是方法、构造函数还是属性获取器)的开销已经大大降低。典型的收益是3-4倍的速度。

使用BenchmarkDotNet 包:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Reflection;

namespace ReflectionBenchmarks
{
    internal class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<InvokeTest>();
        }
    }

    public class InvokeTest
    {
        private MethodInfo? _method;
        private object[] _args = new object[1] { 42 };

        [GlobalSetup]
        public void Setup()
        {
            _method = typeof(InvokeTest).GetMethod(nameof(InvokeMe), BindingFlags.Public | BindingFlags.Static)!;
        }

        [Benchmark]
        // *** This went from ~116ns to ~39ns or 3x (66%) faster.***
        public void InvokeSimpleMethod() => _method!.Invoke(obj: null, new object[] { 42 });

        [Benchmark]
        // *** This went from ~106ns to ~26ns or 4x (75%) faster. ***
        public void InvokeSimpleMethodWithCachedArgs() => _method!.Invoke(obj: null, _args);

        public static int InvokeMe(int i) => i;
    }
}

ML.NET文本分类API

#835

文本分类是对文本应用标签或类别的过程。

常见的用例包括:

  • 将电子邮件归类为垃圾邮件或非垃圾邮件
  • 分析客户评论中的情绪是积极的还是消极的
  • 将标签应用于支持票据

文本分类是分类的一个子集,所以今天你可以用ML.NET中现有的分类算法解决文本分类问题。然而,这些算法并没有像现代深度学习技术那样解决文本分类的常见挑战。

我们很高兴地介绍ML.NET文本分类API,这个API使你更容易训练自定义文本分类模型,并将最新的自然语言处理的最先进的深度学习技术带到ML.NET。

更多细节请见我们的ML.NET具体公告

代码生成

Arm64

#68363合并了'msub'(两个寄存器值相乘,从第三个寄存器值中减去乘积)和'madd'(两个寄存器值相乘,增加第三个寄存器值)逻辑。

Arm64:让CpBlkUnroll和InitBlkUnroll使用SIMD寄存器来初始化复制一个小于128字节的内存块(见perf改进细节)。

image.png

循环优化

#67930 处理更多的循环克隆场景,现在支持向后或向前增量为>1的循环(见perf改进细节)。

image.png

#68588 悬挂 "this "对象的空检查,将空检查移至循环外的对象上(见perf改进细节)。

image.png

x86/x64优化

一般优化

  • PR#68105启用了多个嵌套的 "无GC "区域请求。
  • PR#69034删除了 "促进参数 "的尾随调用限制。

现代化的JIT

随着社区对JIT代码库贡献的增加,重组和现代化我们的代码库变得非常重要,以使我们的贡献者能够轻松提升和快速开发代码。

在Preview 5中,我们在内部做了大量的工作,清理了JIT的中间表示,并消除了过去设计决定所带来的限制。在许多情况下,这些工作导致了更少的内存使用更高的JIT本身的吞吐量,而在其他情况下,则导致了更好的代码质量。下面是一些亮点:

上述内容使我们能够消除JIT的内联器在内联带有字节/字节/短线/短线类型参数的函数时的旧限制,从而提高了代码质量(允许内联器替代小参数#69068)。

需要改进的一个领域是更好地理解涉及结构体和结构体字段的读写的不安全代码@SingleAccretion通过将JIT的内部模型转换为更普遍的 "物理 "模型,在这个领域贡献了巨大的变化。这为JIT使用结构重释等功能更好地推理不安全代码铺平了道路。

  • 物理值的编号#68712
  • 为VNF_BitCast实现常量折叠#68979

为了简化JIT的IR,还进行了其他小的清理:

  • 删除GTF_LATE_ARG#68617
  • 在内联候选参数中替换GT_RET_EXPR#69117
  • 删除LIR中作为调用操作数的存储#68460

以.NET 7为目标

要以.NET 7为目标,你需要在你的项目文件中使用一个.NET 7目标框架名称(TFM)。比如说:

<TargetFramework>net7.0</TargetFramework>

全套的.NET 7 TFMs,包括具体操作的TFMs如下:

  • net7.0
  • net7.0-android
  • net7.0-ios
  • net7.0-maccatalyst
  • net7.0-macos
  • net7.0-tvos
  • net7.0-windows

我们希望从.NET 6升级到.NET 7应该是很简单的。请报告您在用.NET 7测试现有应用程序过程中发现的任何破坏性变化。

支持

.NET 7是一个**短期支持(STS)**版本,这意味着它将在发布之日起的18个月内获得免费支持和补丁。值得注意的是,所有版本的质量都是一样的。唯一的区别是支持的长度。关于.NET支持政策的更多信息,请参阅.NET和.NET Core官方支持政策

我们最近最近将 "当前 "的名称改为 "短期支持(STS)"。我们正在推出这一变化

突破性变化

你可以通过阅读《.NET 7中的突破性变化》文件,找到最新的.NET 7突破性变化列表。它按领域和版本列出了突破性变化,并附有详细解释的链接。

要想知道哪些破坏性变化是被提议的,但仍在审查中,请关注提议的.NET破坏性变化GitHub问题

路线图

.NET的发布包括产品、库、运行时间和工具,并代表了微软内部和外部多个团队的合作。你可以通过阅读产品路线图了解这些领域的更多信息:

闭幕

我们赞赏并感谢您对.NET的支持和贡献。请试一试.NET 7 Preview 5,并告诉我们你的想法!