.NET 7预览版6发布——亮点展示

234 阅读13分钟

今天我们发布了.NET 7预览版6。这个.NET 7预览版包括对类型转换器的改进、JSON合约的定制、System.Formats.Tar API的更新、对.NET模板创作的约束以及CodeGen领域的性能提升。

你可以下载.NET 7预览6,适用于Windows、macOS和Linux:

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

类型转换器

对于新增加的原始类型DateOnly,TimeOnly,Int128,UInt128, 和Half ,现在有暴露的类型转换器:

namespace System.ComponentModel
{
    public class DateOnlyConverter : System.ComponentModel.TypeConverter
    {
        public DateOnlyConverter() { }
    }

    public class TimeOnlyConverter : System.ComponentModel.TypeConverter
    {
        public TimeOnlyConverter() { }
    }

    public class Int128Converter : System.ComponentModel.BaseNumberConverter
    {
        public Int128Converter() { }
    }

    public class UInt128Converter : System.ComponentModel.BaseNumberConverter
    {
        public UInt128Converter() { }
    }

    public class HalfConverter : System.ComponentModel.BaseNumberConverter
    {
        public HalfConverter() { }
    }
}

使用实例

TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly));
// produce DateOnly value of DateOnly(1940, 10, 9)
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;

TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// produce TimeOnly value of TimeOnly(20, 30, 50)
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;

TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// produce Half value of -1.2
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;

TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// produce Int128 value of Int128.MaxValue which equal 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;

TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// produce UInt128 value of UInt128.MaxValue which equal 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;

JSON合约定制

在某些情况下,对JSON进行序列化或反序列化的开发者发现他们不想或不能改变类型,因为它们要么来自外部库,要么会极大地污染代码,但需要做一些影响序列化的改变,如删除属性、改变数字的序列化方式、对象的创建方式等等。他们经常被迫编写包装器或自定义转换器,这不仅是一个麻烦,而且会使序列化变得更慢。

JSON合约的定制允许用户对哪些类型被序列化或反序列化进行更多控制。

选择自定义

开发者有两种基本的方式可以 "插入 "定制,它们最终都会分配给JsonSerializerOptions.TypeInfoResolver ,并需要分配给解析器。

  • 开发者可以使用DefaultJsonTypeInfoResolver 并添加他们的修改器,所有的修改器将被串行调用。
JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers =
        {
            (JsonTypeInfo jsonTypeInfo) =>
            {
                // your modifications here, i.e.:
                if (jsonTypeInfo.Type == typeof(int))
                {
                    jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
                }
            }
        }
    }
};

Point point = JsonSerializer.Deserialize<Point>(@"{""X"":""12"",""Y"":""3""}", options);
Console.WriteLine($"({point.X},{point.Y})"); // (12,3)

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
  • 通过实现System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver 编写自己的自定义解析器。
  • 当类型未被处理时,代码应返回null
  • IJsonTypeInfoResolver 可以与其他解析器结合起来,形成有效的解析器,它将返回第一个非空的答案。例如, 。JsonTypeInfoResolver.Combine(new MyResolver(), new DefaultJsonTypeInfoResolver())

自定义

IJsonTypeInfoResolver 工作是为任何 序列器请求提供 - 这在每个选项的每个类型中只发生一次。 将决定开发者可以改变哪些旋钮,并根据转换器来决定,而转换器是根据提供给选项的转换器来决定的。例如 意味着 可以被添加/修改,而 意味着没有一个旋钮被保证使用 - 这在类型有自定义转换器时可能发生。Type JsonTypeInfo``JsonTypeInfo.Kind JsonTypeInfoKind.Object Properties JsonTypeInfoKind.None

JsonTypeInfo DefaultJsonTypeInfoResolver JsonTypeInfo.CreateJsonTypeInfo - 从头开始创建意味着用户还需要设置 。JsonTypeInfo.CreateObject

定制属性

属性只有在JsonTypeInfo.Kind == JsonTypeInfoKind.Object ,在DefaultJsonTypeInfoResolver 的情况下才会被预先填充。它们可以通过使用JsonTypeInfo.CreateJsonPropertyInfo 来修改或创建,并添加到属性列表中,也就是说,假设你从单独的库中得到一个类,它有奇怪的设计API,你不能改变:

class MyClass
{
    private string _name = string.Empty;
    public string LastName { get; set; }

    public string GetName() => _name;
    public void SetName(string name)
    {
        _name = name;
    }
}

在这个功能存在之前,你需要包装你的类型层次,或者为该类型创建你自己的自定义转换器。现在你可以简单地修复它:

JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers = { ModifyTypeInfo }
    }
};

MyClass obj = new()
{
    LastName = "Doe"
};

obj.SetName("John");

string serialized = JsonSerializer.Serialize(obj, options); // {"LastName":"Doe","Name":"John"}

static void ModifyTypeInfo(JsonTypeInfo ti)
{
    if (ti.Type != typeof(MyClass))
        return;

    JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "Name");
    property.Get = (obj) =>
    {
        MyClass myClass = (MyClass)obj;
        return myClass.GetName();
    };

    property.Set = (obj, val) =>
    {
        MyClass myClass = (MyClass)obj;
        string value = (string)val;
        myClass.SetName(value);
    };

    ti.Properties.Add(property);
}

属性的有条件序列化

在一些使用场景中,需要对一些默认值不进行序列化。也就是说,你不希望0 ,在JSON中显示出某些属性。以前可以通过使用JsonIgnoreAttributeJsonIgnoreCondition.WhenWritingDefault 。当你的默认值不是0 ,而是不同的东西,即-1 ,或者它取决于一些外部设置时,问题就出现了。

现在可以用你喜欢的任何条件来设置你自己的谓词ShouldSerialize 。例如,你有string 属性,你想让N/A 不在JSON中显示出来。

// string property you'd like to customize
JsonPropertyInfo property = ...;

property.ShouldSerialize = (obj, val) =>
{
    // in this specific example we don't use parent but it's available if needed
    MyClass parentObj = (MyClass)obj;
    string value = (string)val;
    return value != "N/A";
};

实例:忽略具有特定名称或类型的属性

var modifier = new IgnorePropertiesWithNameOrType();
modifier.IgnorePropertyWithType(typeof(SecretHolder));
modifier.IgnorePropertyWithName("IrrelevantDetail");

JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers = { modifier.ModifyTypeInfo }
    }
};

ExampleClass obj = new()
{
    Name = "Test",
    Secret = new SecretHolder() { Value = "MySecret" },
    IrrelevantDetail = 15,
};

string output = JsonSerializer.Serialize(obj, options); // {"Name":"Test"}

class ExampleClass
{
    public string Name { get; set; }
    public SecretHolder Secret { get; set; }
    public int IrrelevantDetail { get; set; }
}

class SecretHolder
{
    public string Value { get; set; }
}

class IgnorePropertiesWithNameOrType
{
    private List<Type> _ignoredTypes = new List<Type>();
    private List<string> _ignoredNames = new List<string>();

    public void IgnorePropertyWithType(Type type)
    {
        _ignoredTypes.Add(type);
    }

    public void IgnorePropertyWithName(string name)
    {
        _ignoredNames.Add(name);
    }

    public void ModifyTypeInfo(JsonTypeInfo ti)
    {
        JsonPropertyInfo[] props = ti.Properties.Where((pi) => !_ignoredTypes.Contains(pi.PropertyType) && !_ignoredNames.Contains(pi.Name)).ToArray();
        ti.Properties.Clear();

        foreach (var pi in props)
        {
            ti.Properties.Add(pi);
        }
    }
}

System.Formats.Tar API更新

在预览4中,引入了System.Formats.Tar 集合。它提供了操作TAR档案的API。

在Preview 6中,为了涵盖一些特殊情况,做了一些改变。

全局扩展属性专用类

最初的设计是假设只有PAX TAR档案可以在第一个位置包含一个单一的全局扩展属性(GEA)条目,但人们发现TAR档案可以包含多个GEA条目,这可以影响所有后续条目,直到遇到一个新的GEA条目或档案的结束。

我们还发现,GEA条目不应该只出现在只包含PAX条目的档案中:它们可以出现在不同格式的条目混合的档案中。因此增加了一个新的类别来描述GEA条目。

+ public sealed partial class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry
+ {
+     public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes) { }
+     public IReadOnlyDictionary<string, string> GlobalExtendedAttributes { get { throw null; } }
+ }

条目格式,而不是档案格式

由于发现不同格式的条目可以在一个TAR档案中混合使用,TarFormat 枚举被重新命名为TarEntryFormat

-public enum TarFormat
+public enum TarEntryFormat
{
    ...
}

并为TarEntry 添加了一个新的属性,以显示条目的格式:

public abstract partial class TarEntry
{
    ...
+    public TarEntryFormat Format { get { throw null; } }
    ...
}

写入和读取的变化

Format 属性已从TarReader 中删除,因为没有任何存档会以单一格式显示其所有条目。

由于GEA条目现在被用他们自己的专业类来描述,而且这种类型的多个条目可以在一个档案中找到,所以TarReader 中的字典属性也被删除:

public sealed partial class TarReader : IDisposable
{
    ...
-    public TarFormat Format { get { throw null; } }
-    public IReadOnlyDictionary<string, string>? GlobalExtendedAttributes { get { throw null; } }
    ...
}

增加专门的GEA类也影响了TarWriter

  • 过去用于获取单个第一位置GEA条目的字典的构造函数被删除。
  • 增加了一个新的构造函数,它只接受流和leaveOpen 布尔值。
  • 保留了获取TarFormat 的构造函数,但重命名了枚举,并将默认值设置为Pax 。该方法的文档被修改,以解释指定的格式参数只适用于从文件中添加条目的TarWriter.WriteEntry 方法。
public sealed partial class TarWriter : IDisposable
{
    ...
-    public TarWriter(Stream archiveStream, IEnumerable<KeyValuePair<string, string>>? globalExtendedAttributes = null, bool leaveOpen = false) { }
+    public TarWriter(Stream archiveStream, bool leaveOpen = false) { }
-    public TarWriter(Stream archiveStream, TarFormat archiveFormat, bool leaveOpen = false) { }
+    public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pax, bool leaveOpen = false) { }
     public void WriteEntry(string fileName, string? entryName) { }
    ...
}

模板创作

约束条件

预览6为.NET模板引入了约束的概念。约束允许你定义允许你的模板的上下文 - 这可以帮助模板引擎确定它应该在dotnet new list 等命令中显示哪些模板。在这个版本中,我们增加了对三种约束的支持:

  • 操作系统 - 根据用户的操作系统限制模板的使用
  • 模板引擎主机 - 根据执行模板引擎的主机来限制模板 - 这通常是指.NET CLI本身,或者像Visual Studio/Visual Studio for Mac中的新项目对话框这样的嵌入式方案。
  • 已安装的工作负载--要求在模板可用之前,指定的.NET SDK工作负载已经安装。

在所有情况下,描述这些约束条件就像在你的模板配置文件中添加一个新的constraints 部分一样简单:

 "constraints": {
       "web-assembly": {
           "type": "workload",
           "args": "wasm-tools"
       },
   }

这些模板可以被命名,在通知用户为什么不能调用你的模板时,我们会使用这个名字。

目前,这些约束条件在.NET CLI中得到了支持,我们正在与Visual Studio团队的合作伙伴合作,将它们纳入你已经知道的项目和项目创建体验中。

我们希望这个功能将为SDK的用户带来更一致的体验,无论他们选择什么样的编辑器,都能更容易地指导用户使用必要的模板前提条件,同时也能帮助我们为常见的场景(如dotnet new list )简化模板列表。在未来的.NET 7预览版中,我们计划增加对基于常见的MSBuild属性的约束的支持。

更多的例子请参见约束条件的文档,关于新类型的约束条件的讨论请加入模板引擎仓库的讨论

多选参数

预览6还为choice 参数增加了一个新的能力--用户可以在一次选择中指定一个以上的值。这可以用与Flags 风格的枚举相同的方式来使用。这种类型的参数的常见例子可能是:

  • web 模板上选择多种形式的认证
  • maui 模板中一次选择多个目标平台(ios、android、web)。

选择这种行为就像在你的模板配置中的参数定义中添加"allowMultipleValues": true 一样简单。一旦你这样做,你将获得一些辅助函数,在你的模板内容中使用,以帮助检测用户选择的特定值。

关于该功能的完整解释,请参见多选参数的文档

退出代码的统一和报告

预览6还统一了由模板引擎报告的退出代码。这应该可以帮助那些在他们选择的shell中依赖脚本的用户有一个更一致的错误处理经验。此外,由.NET CLI报告的错误现在包括一个链接,以找到有关每个退出代码的详细信息:

➜ dotnet new unknown-template
No templates found matching: 'unknown-template'.

To list installed templates, run:
   dotnet new list
To search for the templates on NuGet.org, run:
   dotnet new search unknown-template

For details on the exit code, refer to https://aka.ms/templating-exit-codes#103

动态PGO

  • github.com/dotnet/runt…中增加了对委托调用的防护性去虚拟化的支持。当动态PGO被启用时,它允许JIT在确定可能有利可图时对委托调用进行专门化和内联。这可以极大地提高性能,下面的微测试证明了这一点,动态PGO现在比没有PGO的情况下快了大约5倍(之前大约是2.5倍)。

目前只支持与实例方法绑定的委托。我们期望对静态方法的支持将在.NET 8的早期预览中出现。

public class Benchmark
{
    private readonly long[] _nums;
    public Benchmark()
    {
        _nums = Enumerable.Range(0, 100000).Select(i => (long)i).ToArray();
    }

    [Benchmark]
    public long Sum() => _nums.Sum(l => l * l);
}
方法工作工具链意思是误差StdDev比值
总数工作-QWNDLL\nopgo\corerun.exe406.65 us0.718 us0.560 us1.00
总数工作-PNPEDU\Ttieredpgo_no_delegate_gdv\corerun.exe172.77 us0.819 us0.766 us0.42
总数工作-KFFWQK\ティンバーション_ドライト_グループラン.exe91.38 us0.263 us0.219 us0.22
  • 我们开始实施冷热分割github.com/dotnet/runt…,这是它的第一部分。
    • 在JIT(PR)中已经实现了ARM64上的热/冷分割。这项工作主要包括为热/冷段之间的分支生成长的伪指令,以及从数据段加载常量。
    • 我们还增加了对带有异常处理的热/冷分割函数的支持(PR)。在没有PGO数据的情况下,我们的启发式方法将所有的异常处理函数移到冷段,并将 "finally "块复制到热段;我们的操作是假设异常很少发生,但无论是否存在异常,finally块都要执行。
    • 当运行各种SuperPMI集合时,JIT在低端(无PGO数据)分割了14%的函数,在高端(有PGO数据)分割了26%的函数。在这里看到更多的指标。

Arm64

image

循环优化

  • 由类型测试驱动的循环克隆:https://github.com/dotnet/runtime/pull/70377,使循环克隆基于循环不变的类型测试,如GDV增加的那些。这有效地允许快速路径循环将类型检查从循环中提升出来,从而提高性能。比如说:

Announcing .NET 7 Preview 6

image

一般优化

  • PRgithub.com/dotnet/runt…改进了JIT中对矢量常量的处理,包括对值编号的支持、常量传播以及其他已经可用于其他常量的优化。

以.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预览版6,并告诉我们你的想法!