今天我们发布了 .NET 7 预览版 6。.NET 7 的这个预览版包括对类型转换器的改进、JSON 合约可自定义、System.Formats.Tar API 更新、对 .NET 模板创作的约束以及 CodeGen 领域的性能增强。
您可以下载适用于 Windows、macOS 和 Linux 的 .NET 7 Preview 6。
.NET 7 Preview 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));
// 产生 DateOnly(1940, 10, 9) 的 DateOnly 值
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;
TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// 产生 TimeOnly(20, 30, 50) 的 TimeOnly 值
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;
TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// 产生 -1.2 的一半值
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;
TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// 产生 Int128 的 Int128 值。最大值 等于 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;
TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// 产生 UInt128 的 UInt128 值。最大值 等于 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;
JSON合约定制
在某些情况下,序列化或反序列化 JSON 的开发人员发现他们不想或不能更改类型,因为它们要么来自外部库,要么会因为需要进行一些影响序列化的更改而严重污染代码例如删除属性、更改数字的序列化方式,以及对象的创建方式。开发人员经常被迫编写包装器或自定义转换器,这不仅很麻烦,而且会使序列化变慢。
JSON 合约可自定义允许用户更好地控制类型序列化或反序列化的内容和方式。
选择定制
开发人员可以通过两种基本方式“插入”自定义,它们最终都会分配JsonSerializerOptions.TypeInfoResolver 并需要分配解析器:
- 开发者可以使用 DefaultJsonTypeInfoResolver 并添加其修饰符,所有修饰符将被串行调用:
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
(JsonTypeInfo jsonTypeInfo) =>
{
// 您在此处的修改,即:
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 的工作是为任何类型序列化器请求提供 JsonTypeInfo - 每个选项每个类型只会发生一次。 JsonTypeInfo.Kind将确定开发人员可以更改哪些旋钮并根据转换器确定,该转换器基于提供给选项的转换器确定。 例如JsonTypeInfoKind.Object 意味着可以添加/修改属性,而 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 中的某些属性里。之前可以通过将JsonIgnoreAttribute与 JsonIgnoreCondition.WhenWritingDefault
一起使用来使该场景正常工作。 但是当您的默认值为非0例如-1时或者当您的默认值取决于外部设置时,就会出现问题。
现在您可以使用您想要的任何条件设置您自己的谓词ShouldSerialize。 例如您设置了一个字符串属性并且您希望 N/A不会出现在 JSON 中:
// 您要自定义的字符串属性
JsonPropertyInfo property = ...;
property.ShouldSerialize = (obj, val) =>
{
// 在这个特定的例子中,我们不使用 parent 但如果需要它是可用的
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 更新
在 Preview 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": {
"web-assembly": {
"type": "workload",
"args": "wasm-tools"
},
}
这些模板可以命名,我们将在通知用户为什么他们不能调用您的模板时使用该名称。
目前,.NET CLI 支持这些约束,我们正在与 Visual Studio 团队中的合作伙伴合作,将它们整合到您已经知道的项目和项目创建体验中。
我们希望此功能将为 SDK 用户带来更一致的体验,无论他们选择何种编辑器,更容易引导用户了解必要的模板先决条件,并帮助我们整理常见场景的模板列表,例如 dotnet 新列表。 在 .NET 7 的未来预览版中,我们计划添加对基于通用 MSBuild 属性的约束的支持!
有关更多示例,请参阅约束文档,有关新类型约束的讨论,请加入模板引擎存储库中的讨论。
多选参数
预览版 6 还为choice参数添加了一项新功能——用户可以在单个选择中指定多个值。这可以像使用Flags-style 枚举一样使用。此类参数的常见示例可能是:
- 在 web 模板上选择多种形式的身份验证
- 在maui模板中一次选择多个目标平台(ios、android、web)
选择加入此行为就像在模板配置中的参数定义中添加 "allowMultipleValues":
true 一样简单。 完成后,您将可以访问许多用于模板内容的辅助函数,以帮助检测用户选择的特定值。
有关该功能的完整说明,请参阅多选参数文档。
退出代码统一和报告
Preview 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
CodeGen
动态 PGO
- https://github.com/dotnet/run...增加了对委托调用的保护去虚拟化的支持。启用动态 PGO 后,当 JIT 确定这可能有利可图时,它允许 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);
}
- 我们开始实现冷热分离,https://github.com/dotnet/run...是它的第一部分。
ARM64 上的热/冷分离已在 JIT (PR) 中实现。这项工作主要包括生成用于在热/冷部分之间分支的长伪指令,以及从数据部分加载常量。
我们还添加了对带有异常处理 (PR) 的函数的热/冷拆分的支持。如果没有 PGO 数据,我们的启发式算法会将所有异常处理函数移动到冷段,并将“finally”块复制到热段;我们是在异常很少发生的假设下运行的,但是无论是否存在异常,都会执行 finally 块。
在运行各种 SuperPMI 集合时,JIT 将约 14% 的低端函数(无 PGO 数据)和约 26% 的高端函数(有 PGO 数据)拆分。 在此处查看更多指标。
Arm64
- https://github.com/dotnet/run...在 Windows Arm64 中启用了 LSE 原子。它将锁相关的操作性能提高了 78%。
- https://github.com/dotnet/run... 在 Arm64 上启用 gc 类型的寻址模式以获得高达 45% 的性能。
- https://github.com/dotnet/run... 对齐 16 字节 SIMD16 的 arm64 数据部分。
- https://github.com/dotnet/run... 优化 i % 2 并提供高达 17% 的吞吐量提升。
循环优化
- 由类型测试驱动的循环克隆:
https ://github.com/dotnet/runtime/pull/70377 启用基于循环不变类型测试的循环克隆,例如 GDV 添加的那些。这有效地允许快速路径循环将类型检查提升到循环之外,从而提高性能。例如:
- 开始从 https://github.com/dotnet/run... 中的多级嵌套循环中提升不变量。
一般优化
- PR https://github.com/dotnet/run...改进了 JIT 中向量常量的处理,包括对值编号、常量传播和其他常量已经可用的其他优化的支持。
面向 .NET 7
要面向 .NET 7,您需要在项目文件中使用 .NET 7 Target Framework Moniker (TFM)。例如:
<TargetFramework>net7.0</TargetFramework>
全套 .NET 7 TFM,包括特定于操作的 TFM。
- 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 重大更改列表。它按区域和版本列出了重大更改,并附有详细说明的链接。
要查看提出了哪些重大更改但仍在审核中,请关注Proposed .NET Breaking Changes GitHub issue。
路线图
.NET 版本包括产品、库、运行时和工具,代表了 Microsoft 内外多个团队之间的协作。您可以通过阅读产品路线图了解有关这些领域的更多信息:
结束
我们感谢您对 .NET 的所有支持和贡献。请尝试 .NET 7 Preview 6并告诉我们您的想法!
长按识别二维码
关注微软中国MSDN
点击下载 .NET 7 Preview 6 ~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。