修剪|.NET 6 攻略大全(三)
点击上方蓝字
关注我们
(本文阅读时间:15分钟)
.NET 6 继续与大家相约周日啦 。 本篇文章将介绍:单文件应用、IL 修整、System.Text.Json、源代码构建、库AIP的相关攻略 。
单文件应用
在 .NET 6中 , 已为 Windows 和 macOS 启用内存中单文件应用程序 。 在 .NET 5 中 , 这种部署类型仅限于 Linux 。 您现在可以为所有受支持的操作系统发布作为单个文件部署和启动的单文件二进制文件 。 单文件应用不再将任何核心运行时程序集提取到临时目录 。
这种扩展功能基于称为“超级主机”的构建块 。 “apphost” 是在非单文件情况下启动应用程序的可执行文件 , 例如 myapp.exe或./myapp. Apphost 包含用于查找运行时、加载它并使用该运行时启动您的应用程序的代码 。 Superhost 仍然执行其中一些任务 , 但使用所有 CoreCLR 本机二进制文件的静态链接副本 。 静态链接是我们用来实现单一文件体验的方法 。 本机依赖项(如 NuGet 包附带的)是单文件嵌入的显着例外 。 默认情况下 , 它们不包含在单个文件中 。 例如 , WPF 本机依赖项不是超级主机的一部分 , 因此会在单文件应用程序之外产生其他文件 。 您可以使用该设置 IncludeNativeLibrariesForSelfExtract 嵌入和提取本机依赖项 。
▌静态分析
我们改进了单文件分析器以允许自定义警告 。 如果您的 API 在单文件发布中不起作用 , 您现在可以使用[RequiresAssemblyFiles]属性对其进行标记 , 如果启用了分析器 , 则会出现警告 。 添加该属性还将使方法中与单个文件相关的所有警告静音 , 因此您可以使用该警告将警告向上传播到您的公共 API 。
当 PublishSingleFile 设置为 true 时 , 会自动为 exe 项目启用单文件分析器 , 但您也可以通过将 EnableSingleFileAnalysis 设置为 true 来为任何项目启用它 。 如果您想支持将库作为单个文件应用程序的一部分 , 这将很有帮助 。
在 .NET 5 中 , 我们为单文件包中行为不同的 Assembly.Location 和一些其他 API 添加了警告 。
▌压缩
单文件包现在支持压缩 , 可以通过将属性设置 EnableCompressionInSingleFile为true. 在运行时 , 文件会根据需要解压缩到内存中 。 压缩可以为某些场景节省大量空间 。
让我们看一下与 NuGet 包资源管理器一起使用的单个文件发布(带压缩和不带压缩) 。
无压缩:172 MB
文章图片
压缩:71.6 MB
文章图片
压缩会显着增加应用程序的启动时间 , 尤其是在 Unix 平台上 。 Unix 平台有一个不能用于压缩的无拷贝快速启动路径 。 您应该在启用压缩后测试您的应用程序 , 看看额外的启动成本是否可以接受 。
▌单文件调试
目前只能使用平台调试器(如 WinDBG)来调试单文件应用程序 。 我们正在考虑使用更高版本的 Visual Studio 2022 添加 Visual Studio 调试 。
▌macOS 上的单文件签名
单文件应用程序现在满足 macOS 上的 Apple 公证和签名要求 。 具体更改与我们根据离散文件布局构建单文件应用程序的方式有关 。
Apple 开始对macOS Catalina实施新的签名和公证要求 。 我们一直在与 Apple 密切合作 , 以了解需求 , 并寻找使 .NET 等开发平台能够在该环境中正常工作的解决方案 。 我们已经进行了产品更改并记录了用户工作流程 , 以满足 Apple 在最近几个 .NET 版本中的要求 。 剩下的差距之一是单文件签名 , 这是在 macOS 上分发 .NET 应用程序的要求 , 包括在 macOS 商店中 。
IL 修整
该团队一直致力于为多个版本进行 IL 修整 。 .NET 6 代表了这一旅程向前迈出的重要一步 。 我们一直在努力使更激进的修剪模式安全且可预测 , 因此有信心将其设为默认模式 。 TrimMode=link 以前是可选功能 , 现在是默认功能 。
我们有一个三管齐下的修剪策略:
- 提高平台的修剪能力 。
- 对平台进行注释以提供更好的警告并使其他人也能这样做 。
- 在此基础上 , 让默认的修剪模式更具侵略性 , 以便让应用程序变小 。
▌减小应用程序大小
让我们使用SDK 工具之一的crossgen来看看这个修剪改进 。 它可以通过几个修剪警告进行修剪 , crossgen 团队能够解决 。
首先 , 让我们看一下将 crossgen 发布为一个独立的应用程序而无需修剪 。 它是 80 MB(包括 .NET 运行时和所有库) 。
然后我们可以尝试(现在是旧版).NET 5 默认修剪模式 , copyused. 结果降至 55 MB 。
新的 .NET 6 默认修剪模式link将独立文件大小进一步降低到 36MB 。
我们希望新的link修剪模式能更好地与修剪的期望保持一致:显着节省和可预测的结果 。
默认启用警告
修剪警告告诉您修剪可能会删除运行时使用的代码的地方 。 这些警告以前默认禁用 , 因为警告非常嘈杂 , 主要是由于 .NET 平台没有参与修剪作为第一类场景 。
我们对大部分 .NET 库进行了注释 , 以便它们产生准确的修剪警告 。 因此 , 我们觉得是时候默认启用修剪警告了 。 ASP.NET Core 和 Windows 桌面运行时库尚未注释 。 我们计划接下来注释 ASP.NET 服务组件(在 .NET 6 之后) 。 我们希望看到社区在 .NET 6 发布后对 NuGet 库进行注释 。
您可以通过设置<SuppressTrimAnalysisWarnings>为true来禁用警告 。
更多信息:
- 修剪警告
- 修剪介绍
- 准备 .NET 库以进行修剪
我们也为Native AOT 实验实现了相同的修剪警告 , 这应该会以几乎相同的方式改善 Native AOT 编译体验 。
数学
我们显着改进了数学 API 。 社区中的一些人已经在享受这些改进 。
面向性能的 API
System.Math 中添加了面向性能的数学 API 。 如果底层硬件支持 , 它们的实现是硬件加速的 。
新 API:
- SinCos 用于同时计算 Sin 和 Cos 。
- ReciprocalEstimate 用于计算 1 / x的近似值 。
- ReciprocalSqrtEstimate 用于计算1 / Sqrt(x) 的近似值 。
- Clamp , DivRem , Min 和 Max 支持 nint 和 nuint 。
- Abs 和 Sign支持 nint 。
- DivRem 变体返回 tuple 。
ScaleB被移植到 C# 导致调用速度提高了 93% 。 感谢亚历克斯·科文顿 。
大整数性能
改进了从十进制和十六进制字符串中解析 BigIntegers。 我们看到了高达 89% 的改进 , 如下图所示(越低越好) 。
感谢约瑟夫·达席尔瓦 。
▌ComplexAPI 现在注释为 readonly
现在对各种 API 进行了注释 , System.Numerics.Complexreadonly 以确保不会对readonly值或传递的值进行复制 in 。
归功于 hrrrrustic。
▌BitConverter现在支持浮点到无符号整数位广播
BitConverter 现在支持 DoubleToUInt64Bits, HalfToUInt16Bits, SingleToUInt32Bits, UInt16BitsToHalf, UInt32BitsToSingle, 和UInt64BitsToDouble. 这应该使得在需要时更容易进行浮点位操作 。
归功于 Michal Petryka。
▌BitOperations支持附加功能
BitOperations现在支持IsPow2,RoundUpToPowerOf2和提供nint/nuint重载现有函数 。
感谢约翰凯利、霍耀源和罗宾林德纳 。
▌Vector<T>, Vector2, Vector3, 和Vector4改进
Vector<T> 现在支持C# 9 中添加的原始类型nint和nuint原始类型 。 例如 , 此更改应该可以更简单地使用带有指针或平台相关长度类型的 SIMD 指令 。
Vector<T> 现在支持一种Sum方法来简化计算向量中所有元素的“水平和”的需要 。 归功于伊万兹拉塔诺夫 。
Vector<T> 现在支持一种通用方法As<TFrom, TTo>来简化在具体类型未知的通用上下文中处理向量 。 感谢霍耀源
重载支持Span<T>已添加到Vector2、Vector3和Vector4以改善需要加载或存储矢量类型时的体验 。
▌更好地解析标准数字格式
我们改进了标准数字类型的解析器 , 特别是.ToString和.TryFormatParse 。 他们现在将理解对精度 > 99 位小数的要求 , 并将为那么多位数提供准确的结果 。 此外 , 解析器现在更好地支持方法中的尾随零 。
以下示例演示了之前和之后的行为 。
- 32.ToString("C100")->C132
- .NET 5:我们在格式化代码中人为限制只能处理 <= 99 的精度 。 对于精度 >= 100 , 我们改为将输入解释为自定义格式 。
- 32.ToString("H99")-> 扔一个FormatException
- .NET 6:抛出 FormatException 。
- 这是正确的行为 , 但在这里调用它是为了与下一个示例进行对比 。
- 32.ToString("H100")->H132
- .NET 6:抛出 FormatException 。
- .NET 5:H是无效的格式说明符 。 所以 , 我们应该抛出一个FormatException. 相反 , 我们将精度 >= 100 解释为自定义格式的错误行为意味着我们返回了错误的值 。
- .NET 5:9007199254740997.0不能完全以 IEEE 754 格式表示 。 使用我们当前的舍入方案 , 正确的返回值应该是9007199254740996. 但是 , 输入的最后一部分迫使解析器错误地舍入结果并返回 .09007199254740998 。
System.Text.Json 提供多种高性能 API 用于处理 JSON 文档 。 在过去的几个版本中 , 我们添加了新功能 , 以进一步提高 JSON 处理性能并减轻对希望从NewtonSoft.Json迁移的人的阻碍 。 此版本包括在该路径上的继续 , 并且在性能方面向前迈出了一大步 , 特别是在序列化程序源生成器方面 。
▌JsonSerializer 源生成
注意:使用 .NET 6 RC1 或更早版本的源代码生成的应用程序应重新编译 。
几乎所有 .NET 序列化程序的支柱都是反射 。 反射对于某些场景来说是一种很好的能力 , 但不能作为高性能云原生应用程序(通常(反)序列化和处理大量 JSON 文档)的基础 。 反射是启动、内存使用和程序集修整的问题 。
运行时反射的替代方法是编译时源代码生成 。 在 .NET 6 中 , 我们包含一个新的源代码生成器作为System.Text.Json. JSON 源代码生成器可以与多种方式结合使用JsonSerializer并且可以通过多种方式进行配置 。
它可以提供以下好处:
? 减少启动时间
? 提高序列化吞吐量
? 减少私有内存使用
? 删除运行时使用System.Reflection 和System.Reflection.Emit
? IL 修整兼容性
默认情况下 , JSON 源生成器为给定的可序列化类型发出序列化逻辑 。 JsonSerializer通过生成直接使用的源代码 , 这提供了比使用现有方法更高的性能Utf8JsonWriter 。 简而言之 , 源代码生成器提供了一种在编译时为您提供不同实现的方法 , 以使运行时体验更好 。
给定一个简单的类型:
使用此模式的序列化程序调用可能类似于以下示例 。 此示例提供了可能的最佳性能 。
// Writer contains:// {"Message":"Hello, world!"}
最快和最优化的源代码生成模式——基于Utf8JsonWriter——目前仅可用于序列化 。 Utf8JsonReader根据您的反馈 , 将来可能会提供对反序列化的类似支持 。
源生成器还发出类型元数据初始化逻辑 , 这也有利于反序列化 。 JsonMessage要反序列化使用预生成类型元数据的实例 , 您可以执行以下操作:
您现在可以使用System.Text.Json (反)序列化IAsyncEnumerable<T>JSON数组 。 以下示例使用流作为任何异步数据源的表示 。 源可以是本地计算机上的文件 , 也可以是数据库查询或 Web 服务 API 调用的结果 。
JsonSerializer.SerializeAsync已更新以识别并为IAsyncEnumerable值提供特殊处理 。
using Stream stream = Console.OpenStandardOutput;var data = https://www.sohu.com/a/new { Data = PrintNumbers(3) };await JsonSerializer.SerializeAsync(stream, data); // prints {"Data":[0,1,2]}
IAsyncEnumerable 仅使用异步序列化方法支持值 。 尝试使用同步方法进行序列化将导致NotSupportedException被抛出 。
流式反序列化需要一个新的 API 来返回 I AsyncEnumerable<T>. 我们为此添加了JsonSerializer.DeserializeAsyncEnumerable方法 , 您可以在以下示例中看到 。
此示例将按需反序列化元素 , 并且在使用特别大的数据流时非常有用 。 它仅支持从根级 JSON 数组读取 , 尽管将来可能会根据反馈放宽 。
现有 DeserializeAsync 方法名义上支持 I AsyncEnumerable<T> , 但在其非流方法签名的范围内 。 它必须将最终结果作为单个值返回 , 如以下示例所示 。
public class MyPoco{public IAsyncEnumerable<int> Data { get; set; }}
在此示例中 , 反序列化器将 IAsyncEnumerable 在返回反序列化对象之前缓冲内存中的所有内容 。 这是因为反序列化器需要在返回结果之前消耗整个 JSON 值 。
▌System.Text.Json:可写 DOM 功能
可写 JSON DOM特性为 System.Text.Json添加了一个新的简单且高性能的编程模型 。 这个新的 API 很有吸引力 , 因为它避免了需要强类型的序列化合约 , 并且与现有的 JsonDocument类型相比 , DOM 是可变的 。
这个新的 API 有以下好处:
- 在使用POCO类型是不可能或不希望的情况下 , 或者当 JSON 模式不固定且必须检查的情况下 , 序列化的轻量级替代方案 。
- 启用对大树子集的有效修改 。 例如 , 可以有效地导航到大型 JSON 树的子部分并从该子部分读取数组或反序列化 POCO 。 LINQ 也可以与它一起使用 。
// Create a new JsonObject using object initializers and array paramsvar jObject = new JsonObject{["MyChildObject"] = new JsonObject{["MyProperty"] = "Hello",["MyArray"] = new JsonArray(10, 11, 12)}};
// Obtain the JSON from the new JsonObjectstring json = jObject.ToJsonString;Console.WriteLine(json); // {"MyChildObject":{"MyProperty":"Hello","MyArray":[10,11,12]}}
// Indexers for property names and array elements are supported and can be chainedDebug.Assert(jObject["MyChildObject"]["MyArray"][1].GetValue<int> == 11);
▌ReferenceHandler.IgnoreCycles
JsonSerializer(System.Text.Json)现在支持在序列化对象图时忽略循环的能力 。 该ReferenceHandler.IgnoreCycles选项具有与 Newtonsoft.Json ReferenceLoopHandling.Ignore类似的行为 。 一个关键区别是 System.Text.Json 实现用null JSON 标记替换引用循环 , 而不是忽略对象引用 。
您可以在以下示例中看到ReferenceHandler.IgnoreCycles 的行为 。 在这种情况下 , 该Next属性被序列化为null , 因为否则它会创建一个循环 。
var opts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };
string json = JsonSerializer.Serialize(node, opts);Console.WriteLine(json); // Prints {"Deion":"Node 1","Next":null}}
源代码构建
通过源代码构建 , 您只需几个命令即可在您自己的计算机上从源代码构建 .NET SDK。 让我解释一下为什么这个项目很重要 。
源代码构建是一个场景 , 也是我们在发布 .NET Core 1.0 之前一直与 Red Hat 合作开发的基础架构 。 几年后 , 我们非常接近于交付它的全自动版本 。 对于 Red Hat Enterprise Linux (RHEL) .NET 用户来说 , 这个功能很重要 。 Red Hat 告诉我们 , .NET 已经发展成为其生态系统的重要开发者平台 。 好的!
Linux 发行版的黄金标准是使用作为发行版存档一部分的编译器和工具链构建开源代码 。 这适用于 .NET 运行时(用 C++ 编写) , 但不适用于任何用 C# 编写的代码 。 对于 C# 代码 , 我们使用两遍构建机制来满足发行版要求 。 这有点复杂 , 但了解流程很重要 。
Red Hat 使用 .NET SDK (#1) 的 Microsoft 二进制构建来构建 .NET SDK 源代码 , 以生成 SDK (#2) 的纯开源二进制构建 。 之后 , 使用这个新版本的 SDK (#2) 再次构建相同的 SDK 源代码 , 以生成可证明的开源 SDK (#3) 。 .NET SDK (#3) 的最终二进制版本随后可供 RHEL 用户使用 。 之后 , Red Hat 可以使用相同的 SDK (#3) 来构建新的 .NET 版本 , 而不再需要使用 Microsoft SDK 来构建每月更新 。
这个过程可能令人惊讶和困惑 。 开源发行版需要通过开源工具构建 。 此模式确保不需要 Microsoft 构建的 SDK , 无论是有意还是无意 。 作为开发者平台 , 包含在发行版中的门槛比仅使用兼容许可证的门槛更高 。 源代码构建项目使 .NET 能够满足该标准 。
源代码构建的可交付成果是源代码压缩包 。 源 tarball 包含 SDK 的所有源(对于给定版本) 。 从那里 , 红帽(或其他组织)可以构建自己的 SDK 版本 。 Red Hat 政策要求使用内置源工具链来生成二进制 tar 球 , 这就是他们使用两遍方法的原因 。 但是源代码构建本身不需要这种两遍方法 。
在 Linux 生态系统中 , 给定组件同时拥有源和二进制包或 tarball 是很常见的 。 我们已经有了可用的二进制 tarball , 现在也有了源 tarball 。 这使得 .NET 与标准组件模式相匹配 。
.NET 6 的重大改进是源 tarball 现在是我们构建的产品 。 它过去需要大量的人工来制作 , 这也导致将源 tarball 交付给 Red Hat 的延迟很长 。 双方都对此不满意 。
在这个项目上 , 我们与红帽密切合作五年多 。 它的成功在很大程度上要归功于我们有幸与之共事的优秀红帽工程师的努力 。 其他发行版和组织已经并将从他们的努力中受益 。
附带说明一下 , 源代码构建是朝着可重现构建迈出的一大步 , 我们也坚信这一点 。 .NET SDK 和 C# 编译器具有重要的可重现构建功能 。
库 API
除了已经涵盖的 API 之外 , 还添加了以下 API 。
▌WebSocket 压缩
压缩对于通过网络传输的任何数据都很重要 。 WebSockets 现在启用压缩 。 我们使用了WebSockets 的扩展permessage-deflate实现 , RFC 7692 。 它允许使用该DEFLATE算法压缩 WebSockets 消息负载 。 此功能是 GitHub 上 Networking 的主要用户请求之一 。
与加密一起使用的压缩可能会导致攻击 , 例如CRIME和BREACH 。 这意味着不能在单个压缩上下文中将秘密与用户生成的数据一起发送 , 否则可以提取该秘密 。 为了让用户注意到这些影响并帮助他们权衡风险 , 我们将其中一个关键 API 命名为DangerousDeflateOptions 。 我们还添加了关闭特定消息压缩的功能 , 因此如果用户想要发送秘密 , 他们可以在不压缩的情况下安全地执行此操作 。
禁用压缩时 WebSocket的内存占用减少了约 27% 。
从客户端启用压缩很容易 , 如下例所示 。 但是 , 请记住 , 服务器可以协商设置 , 例如请求更小的窗口或完全拒绝压缩 。
归功于伊万兹拉塔诺夫 。
▌Socks 代理支持
SOCKS是一种代理服务器实现 , 可以处理任何 TCP 或 UDP 流量 , 使其成为一个非常通用的系统 。 这是一个长期存在的社区请求 , 已添加到 .NET 6中 。
此更改增加了对 Socks4、Socks4a 和 Socks5 的支持 。 例如 , 它可以通过 SSH 测试外部连接或连接到 Tor 网络 。
该类WebProxy现在接受socks方案 , 如以下示例所示 。
▌Microsoft.Extensions.Hosting — 配置主机选项 API
我们在 IHostBuilder 上添加了一个新的 ConfigureHostOptions API , 以简化应用程序设置(例如 , 配置关闭超时):
在 .NET 5 中 , 配置主机选项有点复杂:
▌Microsoft.Extensions.DependencyInjection — CreateAsyncScope API
CreateAsyncScope 创建API是为了处理服务的处置 IAsyncDisposable 。 以前 , 您可能已经注意到处置 IAsyncDisposable 服务提供者可能会引发 InvalidOperationException 异常 。
以下示例演示了新模式 , CreateAsyncScope 用于启用 using 语句的安全使用 。
// This using can throw InvalidOperationExceptionusing (var scope = provider.CreateScope){var foo = scope.ServiceProvider.GetRequiredService<Foo>;}
class Foo : IAsyncDisposable{public ValueTask DisposeAsync => default;}
以下模式是先前建议的避免异常的解决方法 。 不再需要它 。
▌Microsoft.Extensions.Logging — 编译时源生成器
.NET 6引入了LoggerMessageAttribute类型. 此属性是Microsoft.Extensions.Logging 命名空间的一部分 , 使用时 , 它会源生成高性能日志记录 API 。 源生成日志支持旨在为现代 .NET 应用程序提供高度可用和高性能的日志解决方案 。 自动生成的源代码依赖于 ILogger 接口和 LoggerMessage.Define功能 。
LoggerMessageAttribute 源生成器在用于 partial 日志记录方法时触发 。 当被触发时 , 它要么能够自动生成 partial 它正在装饰的方法的实现 , 要么生成编译时诊断 , 并提供有关正确使用的提示 。 编译时日志记录解决方案在运行时通常比现有的日志记录方法快得多 。 它通过最大限度地消除装箱、临时分配和副本来实现这一点 。
与直接手动使用 LoggerMessage.Define API 相比 , 有以下好处:
- 更短更简单的语法:声明性属性使用而不是编码样板 。
- 引导式开发人员体验:生成器发出警告以帮助开发人员做正确的事情 。
- 支持任意数量的日志记录参数 。 LoggerMessage.Define 最多支持六个 。
- 支持动态日志级别 。 这是 LoggerMessage.Define 单独不可能的 。
▌System.Linq — 可枚举的支持 Index 和 Range 参数
该 Enumerable.ElementAt 方法现在接受来自可枚举末尾的索引 , 如以下示例所示 。
Enumerable.Range(1, 10).ElementAt(^2); // returns 9
添加了一个 Enumerable.Take 接受 Range 参数的重载 。 它简化了对可枚举序列的切片:
- source.Take(..3)代替source.Take(3)
- source.Take(3..)代替source.Skip(3)
- source.Take(2..7)代替source.Take(7).Skip(2)
- source.Take(^3..)代替source.TakeLast(3)
- source.Take(..^3)代替source.SkipLast(3)
- source.Take(^7..^3)而不是.source.TakeLast(7).SkipLast(3)
▌System.Linq —TryGetNonEnumeratedCount
该 TryGetNonEnumeratedCount 方法尝试在不强制枚举的情况下获取源可枚举的计数 。 这种方法在枚举之前预分配缓冲区很有用的场景中很有用 , 如下面的示例所示 。
▌System.Linq — DistinctBy/ UnionBy/ IntersectBy/ExceptBy
新变体已添加到允许使用键选择器函数指定相等性的集合操作中 , 如下例所示 。
▌System.Linq - MaxBy/MinBy
MaxBy 和 MinBy 方法允许使用键选择器查找最大或最小元素 , 如下例所示 。
Chunk可用于将可枚举的源分块为固定大小的切片 , 如下例所示 。
▌System.Linq—— //FirstOrDefault 采用默认参数的重载LastOrDefaultSingleOrDefault
如果源可枚举为空 , 则现有的 FirstOrDefault /LastOrDefault /SingleOrDefault 方法返回 default(T) 。 添加了新的重载 , 它们接受在这种情况下返回的默认参数 , 如以下示例所示 。
▌System.Linq —Zip 接受三个可枚举的重载
Zip方法现在支持组合三个枚举 , 如以下示例所示 。
var zs = xs.Select(x => x % 2 == 0);
foreach ((int x, string y, bool z) in Enumerable.Zip(xs,ys,zs))
{
}
归功于 Huo yaoyuan。
▌优先队列
PriorityQueue<TElement, TPriority>(System.Collections.Generic) 是一个新集合 , 可以添加具有值和优先级的新项目 。 在出队时 , PriorityQueue 返回具有最低优先级值的元素 。 您可以认为这个新集合类似于Queue<T>但每个入队元素都有一个影响出队行为的优先级值 。
以下示例演示了.PriorityQueue<string, int>
pq.Dequeue; // returns "B"pq.Dequeue; // returns "C"pq.Dequeue; // either "A" or "D", stability is not guaranteed.
归功于 Patryk Golebiowski 。
精彩攻略欢迎继续转到下篇文章 , 继续阅读哦!
▎.NET 6 攻略大全(一)
▎.NET 6 攻略大全(二)
【修剪|.NET 6 攻略大全(三)】了解更多.NET
推荐阅读
- 大全|.NET 6 攻略大全(四)
- 仿真|.NET 6 攻略大全(二)
- 基准|ASP.NET Core 6 的性能改进
- Alibaba|2022年阿里云上云采购季大促全攻略
- MacOS|CA周记 | 用 VS Code 做基于 .NET MAUI 跨平台移动应用开发
- 网站|Paperpass论文查重,重复率太高怎么办?毕业论文保过攻略
- Microsoft|微软发文庆祝.NET诞生20周年纪念日!
- Microsoft|喜迎20周年:微软发起.NET直播庆祝活动
- 手机|本周好文:UNiDAYS 认证攻略/ Cubox 玩法/《啫喱》的魔力
- IT|最强攻略:原价买冰墩墩的最全方法都在这儿了