.NET 11 已经不是在单纯“补 API”了,而是在同时解决语言表达能力、异步执行效率、CLI 启动速度、Web 长连接资源占用以及桌面 UI 现代化这些更底层的问题。
尤其是几个变化,我觉得值得单独拿出来聊。
C# 15 开始支持带目标的 break / continue,union 和 closed 类型体系继续完善;Runtime 对 async/await 的优化已经深入到分层编译和 JIT;SDK 开始默认启用 NativeAOT CLI 和 MSBuild Server;基础库第一次加入 IEEE 754 十进制浮点数,还给 ZIP 补上了密码和 AES 加密;ASP.NET Core 则开始让 Blazor Server 的 Circuit 在后台自动“休眠”。Windows Forms 这边也没闲着,新的视觉样式、系统主题变化监听和批量更新暂停绘制都来了。
所以这篇文章不打算照着 Release Notes 一条条翻译,我们直接聊几个我认为真正会影响开发体验的变化。
C# 15:终于可以告诉break,我到底想跳出哪一层循环了
先从最容易看懂的 C# 15 开始。
写过稍微复杂一点循环的人,大概率都碰到过这种代码:
for (int row = 0; row < rows; row++){ bool found = false; for (int col = 0; col < cols; col++) { if (matrix[row, col] == target) { found = true; break; } } if (found) { break; }}
问题不是它不能工作,而是我们的真实意图其实很简单:找到以后,直接结束外层循环。
但过去 C# 的 break 只能退出当前这一层,所以要么加状态变量,要么重构代码,有的人甚至会直接写 goto。
C# 15 在 Preview 7 中给 break 和 continue 加上了“目标”。现在可以直接给循环加标签,然后告诉 break 要退出哪一层。
代码会变成:
search:for (int row = 0; row < rows; row++){ for (int col = 0; col < cols; col++) { if (matrix[row, col] == target) { Console.WriteLine($"找到了:[{row}, {col}]"); break search; } }}
continue 也是一样。
比如处理二维数据时,如果当前行出现某种条件,希望直接处理下一行:
nextRow:for (int row = 0; row < rows; row++){ for (int col = 0; col < cols; col++) { if (ShouldIgnoreRow(row, col)) { continue nextRow; } Process(matrix[row, col]); }}
我觉得这个功能属于那种“看起来特别小,但实际代码里非常舒服”的改进。
它并不是传统意义上的 goto。标签必须直接绑定到 for、foreach、while、do 或 switch 这样的结构上,而且 break / continue 也只能在对应结构内部使用。换句话说,它还是结构化控制流,只是表达能力更强了。
C# 的另一个方向更重要:union和closed正在把类型系统补起来
如果只是 labeled break,那顶多算语法糖。C# 15 更值得关注的其实是 union 和 closed。
Preview 7 继续完善了 union 的模式匹配。现在对一个 union 做 pattern matching 时,编译器既会尝试匹配 union 本身,也会尝试匹配里面真正保存的值。
例如:
public record Dog(string Name);public record Cat(int Lives);public union Pet(Dog, Cat);Pet pet = new Cat(9);if (pet is Cat { Lives: > 0 } cat){ Console.WriteLine($"这只猫还有 {cat.Lives} 条命");}
这意味着以后很多类似:
Result<TSuccess, TError>OneOf<A, B>Message<TextMessage, ImageMessage, FileMessage>
这样的模型,不一定再需要依赖第三方库或者自己设计一套继承体系。
与此同时,closed 类型也越来越完整。
例如:
public closed record Shape;public record Circle(double Radius) : Shape;public record Rectangle(double Width, double Height) : Shape;
一旦基类被定义成 closed,编译器就知道这个继承体系是封闭的。
于是下面这种 switch:
static double GetArea<T>(T shape) where T : Shape{ return shape switch { Circle(var radius) => Math.PI * radius * radius, Rectangle(var width, var height) => width * height };}
编译器能够知道所有直接子类型都已经覆盖,不再因为泛型参数 T 而错误地认为这个 switch 可能遗漏情况。Preview 7 特别补齐的,就是 泛型参数约束到 closed 类型时的 exhaustiveness analysis,也就是穷尽性分析。
这两个特性放到一起看就有意思了。
C# 正在逐渐获得一种过去在 Rust、F#、Swift 这类语言里面比较常见的能力:让编译器更完整地理解“这个值到底可能是什么”。
这对于 Result、状态机、领域模型、编译器、协议解析这一类代码会很有价值。
目前这些 C# 15 特性属于预览能力,测试时项目里可以显式开启 Preview 语言版本:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net11.0</TargetFramework> <LangVersion>preview</LangVersion> </PropertyGroup></Project>
官方在 closed 与 System.Text.Json 的示例中同样明确要求使用 C# Preview 语言版本。
Runtime:async/await 这次真的开始动到底层了
接下来是我觉得 .NET 11 很值得持续关注的一条线:Runtime Async。
过去我们写:
async Task<User> GetUserAsync(){ return await repository.GetUserAsync();}
编译器通常会把这个方法转换成一个异步状态机。
这个设计非常成熟,但状态机意味着更多生成代码,同时涉及状态保存、恢复以及相应的运行时开销。
.NET 11 正在推进的 Runtime Async,则希望把一部分 async/await 的实现能力进一步下沉到 Runtime。
到了 Preview 7,一个很重要的变化是:Runtime Async 版本的异步方法正式进入 Tiered Compilation,也就是分层编译流程。 之前这些方法基本停留在更偏快速启动的 Tier 0,现在热点 async 代码也能够进入更积极优化的 Tier 1。
这里甚至出现了一些非常夸张的优化案例。
官方测试中,一个循环等待已经完成的 Task 的场景,在 JIT 能够把相关 await helper 内联之后,1 亿次调用从大约 191ms 降到了约 32ms。
另一个 TechEmpower platform-json 测试里,因为 tail-await 已经可以在 Tier 0 工作,预热期间记录到的最大分配速率从大约 110MB/s 降到了约 8MB/s。
当然,这不代表你的 Web API 换成 Preview 7 就直接快六倍。
JIT 已经开始理解 async 的语义,而不是只把它当作普通方法调用。
比如这些代码:
return Task.CompletedTask;return Task.FromResult(value);return ValueTask.CompletedTask;return ValueTask.FromResult(value);
以及:
new ValueTask(SomeTask())
JIT 现在能够识别更多常见的 Task / ValueTask factory 和适配器,在合适情况下把不必要的包装直接消掉。
这条路线如果最终成熟,对 ASP.NET Core、数据库客户端、消息系统、RPC 服务这些高度依赖异步调用的项目影响会非常直接。
顺带一提,WebAssembly 上的 CoreCLR 也越来越不像实验品了
.NET 11 还有一条容易被忽略的路线:CoreCLR on WebAssembly。
Preview 6 的时候,CoreCLR 的 WebAssembly 配置已经能够启动;到了 Preview 7,它已经可以端到端运行 .NET Libraries 的测试套件。
背后包括标准 WebAssembly Exception Handling、RyuJIT 的 SIMD 支持、ReadyToRun、WASI host,以及 WebAssembly 下的诊断栈遍历能力。
换句话说,微软并不是简单地说“以后浏览器里也跑 CoreCLR”。
它正在一点点补齐:
运行
→ AOT / R2R→ SIMD→ Exception Handling→ WASI→ Diagnostic→ Libraries compatibility
整条工程链路。
这个方向短期不一定影响普通业务开发,但从 Runtime 架构来看非常值得继续观察。
基础库这次很猛:.NET 终于有 IEEE 754 十进制浮点数了
如果让我从 Libraries 里面挑一个最有“新版本味道”的功能,我会选这个:
Decimal32、Decimal64 和 Decimal128。
它们位于 System.Numerics,分别提供 7 位、16 位和 34 位十进制精度,并实现 IEEE 754-2019 的 decimal floating-point 体系。
这和我们一直使用的 System.Decimal 不是一回事。
它们支持 IEEE 浮点语义,包括 Infinity 和 NaN,同时还能参与 .NET Generic Math。
于是以后可以写:
using System.Numerics;Decimal64 price = Decimal64.Parse("199.90");Decimal64 discount = Decimal64.Parse("0.85");Decimal64 finalPrice = price * discount;Console.WriteLine(finalPrice);
更有意思的是泛型数学。
例如:
using System.Numerics;static T RoundMoney<T>(T value) where T : IFloatingPoint<T>{ return T.Round( value, digits: 2, MidpointRounding.ToEven);}Decimal64 price = Decimal64.Parse("19.995");Console.WriteLine(RoundMoney(price));
同一套泛型算法以后可以覆盖更多数字类型,而不是到处针对 decimal、double 再写一遍。
.NET 11 同时还加入了泛型版本的复数:
Complex<float> value = new Complex<float>(3, 4);Console.WriteLine(value.GetMagnitude());
以前 System.Numerics.Complex 基本绑定 double,现在新的 Complex<T> 可以使用 float、Half、新的 Decimal 浮点类型等,只要类型满足对应的 Generic Math 约束。
这类变化普通 CRUD 项目可能完全感觉不到,但对于科学计算、金融计算、信号处理、数值库作者来说就完全是另一回事了。
一个我特别喜欢的小 API:TryParsePartial
写解析器的人应该很容易理解这个功能为什么实用。
假设现在收到:123;456;789
以前你想基于 Span<T> 高性能解析第一个数字,经常要自己先找分隔符:
var index = input.IndexOf(';');var numberSpan = input[..index];int value = int.Parse(numberSpan);
.NET 11 Preview 7 给 Generic Math 增加了 TryParsePartial。
它不要求整个 Span 都必须是数字,而是告诉你:我成功解析到了哪里。
类似这样:
using System.Globalization;ReadOnlySpan<char> input = "123;456;789";if (int.TryParsePartial( input, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value, out int consumed)){ Console.WriteLine(value); // 123 input = input[consumed..]; Console.WriteLine(input.ToString()); // ;456;789}
对于 CSV、协议解析、日志格式解析以及各种零拷贝 Parser,这种 API 非常舒服,因为不用为了找到字段边界反复切字符串或者复制数据。
HttpClient终于可以比较自然地压缩 Request Body 了
HTTP Response 压缩我们已经很熟悉。
但客户端上传一个很大的 JSON、日志或者批量数据时,过去想压缩 Request Body 往往得自己套 Stream。
.NET 11 Preview 7 在 System.Net.Http 里直接加入了:GZipCompressedContent和BrotliCompressedContent
以及:ZstandardCompressedContent。
比如:
using System.IO.Compression;using System.Net.Http;using var client = new HttpClient();var json = new StringContent( """ { "name": "张三", "message": "Hello .NET 11" } """);using var request = new HttpRequestMessage( HttpMethod.Post, "https://example.com/api/messages");request.Content = new ZstandardCompressedContent( json, CompressionLevel.Optimal);using HttpResponseMessage response = await client.SendAsync(request);response.EnsureSuccessStatusCode();
包装以后,Content-Encoding 等信息会由对应 Content 类型处理,而且内容是流式压缩的。
不过这里有一个很重要的前提:Request Compression 目前是显式选择的,HttpClient 不会自动和服务器协商“你支不支持 zstd”。
所以一定要确认服务器支持对应的编码格式。
ZIP 终于原生支持密码和 AES 了
这个变化估计会解决不少历史代码。
System.IO.Compression 现在可以直接读写有密码的 ZIP Entry,而且支持:
ZipCryptoAES-128AES-192AES-256。
比如:
using System.IO.Compression;const string password = "my-strong-password";using var archive = ZipFile.Open( "backup.zip", ZipArchiveMode.Create);var entry = archive.CreateEntry( "data.txt", password: password, encryptionMethod: ZipEncryptionMethod.Aes256);using var stream = entry.Open(password);using var writer = new StreamWriter(stream);writer.WriteLine("这部分内容已经加密");
读取也一样:
using var archive = ZipFile.OpenRead("backup.zip");var entry = archive.GetEntry("data.txt")!;using var stream = entry.Open("my-strong-password");using var reader = new StreamReader(stream);Console.WriteLine(reader.ReadToEnd());
对于报表导出、数据交换、备份工具这种场景,终于不用为了“ZIP 加个密码”再引入一套额外实现了。
而且 Preview 7 不只是密码,还同时增加了 ZIP 创建、解压的 Options 类型以及更多异步 API。
DNS API 也终于不只会问“这个域名 IP 是什么”
以前很多人认识的 .NET DNS API 基本就是:
Dns.GetHostAddressesAsync(...)
但真实 DNS 世界显然不只有 A / AAAA。
Preview 7 开始提供类型化 DNS 查询,包括 SRV、MX、TXT、CNAME、PTR、NS 等记录,并且返回 DnsResult<T>,里面还能看到 Response Code 和 Negative Cache TTL。
例如:
using System.Net;DnsResult<SrvRecord> result = await Dns.ResolveSrvAsync( "_ldap._tcp.example.com");foreach (SrvRecord record in result.Records){ Console.WriteLine( $"{record.Target}:{record.Port}");}
这对于 Service Discovery、邮件系统、基础设施工具和 DNS 管理程序都很有用。
不过 Preview 7 这里有一个限制:这一版的类型化 DNS Resolver 实现目前首先支持 Windows,其他平台在当前 Preview 中还会抛 PlatformNotSupportedException。
SDK 的变化可能每天都能感觉到:dotnet CLI 开始默认走 NativeAOT
下面聊开发体验。Preview 7 把之前需要手动开启的一项功能直接变成默认行为:NativeAOT dotnet CLI fast path 默认启用。
并不是整个 dotnet CLI 突然全部 AOT 了。
现在的策略更像是:能够由 NativeAOT 路径直接处理的命令,就不要再为了它启动第二套 CoreCLR;需要 MSBuild、NuGet 等复杂能力的时候,再回退到传统 managed CLI。
效果很直观。
微软给出的测试里:
dotnet tool list378 ms → 68 ms
大约快了 5.5 倍。
dotnet dev-certs https、dotnet ef 这类工具分发命令,则从大约 700ms 降到了 200~220ms 左右。
所以它优化的其实是开发过程里那种非常烦人的:
执行命令等一下执行命令又等一下
单次看起来只是几百毫秒,但一天执行几十上百次以后,体感差异就出来了。
如果某些环境出现兼容问题,也可以临时关闭:
DOTNET_CLI_ENABLEAOT=false dotnet --info
MSBuild Server 也默认开启了
另一个类似的优化是 MSBuild Server。
过去每次:
dotnet builddotnet testdotnet run
都可能重新承担一部分 MSBuild 启动成本。
现在 SDK 默认让 MSBuild Server 保持一个 warm worker,也就是保留已经热起来的 MSBuild 工作进程,让后续 CLI 调用避免重复初始化。
这类优化本身不改变你的业务代码,却很可能是日常开发体验提升最明显的东西。
如果想恢复传统单次模式,可以关闭:
export DOTNET_CLI_USE_MSBUILD_SERVER=false
或者:
export MSBUILDUSESERVER=0
所以 .NET 11 SDK 的思路非常清晰:
能不重复启动的东西,就别重复启动。
NativeAOT CLI 解决 dotnet 自己的部分启动成本,MSBuild Server 则解决反复执行工程命令时的 MSBuild 初始化成本。
dotnet test终于能直接规定“最多跑多久”和“最多失败几个”
CI 里面还有一个很实用的功能。
在 Microsoft.Testing.Platform 下,现在可以写:
dotnet test --timeout 90s
意思是整个测试运行最多允许 90 秒。
也可以:
dotnet test --maximum-failed-tests 5
意思是累计失败 5 个以后就停止整次测试。
这个变化看起来不起眼,但放在大型 CI 里非常实际。
假设 5000 个测试里面,前 50 个已经连续炸了 20 个。
以前剩下 4950 个仍然慢慢跑,可能没有任何意义。
现在直接:
dotnet test \ --timeout 10m \ --maximum-failed-tests 20
就可以给整个测试 Session 加一个明确的 fail-fast 策略。
而且这两个参数放在 — 前面时控制的是整个 Run,不只是某一个测试应用。
ASP.NET Core:Blazor Server 的后台标签页终于可以“睡觉”了
我觉得 ASP.NET Core Preview 7 最值得聊的是 Blazor。
Blazor Interactive Server 有一个天然问题:
一个浏览器页面后面对应一个 Circuit。
用户打开页面以后切到别的 Tab,甚至去开会一个小时,这个页面虽然根本没人看,但服务器端 Circuit 仍然可能继续占资源。
Preview 7 引入了 Auto Pause Circuit。
基本配置类似:
<PackageReference Include="Microsoft.AspNetCore.Components.Server.AutoPause" />
然后:
app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .WithBrowserOptions(options => { options.AddAutoPause(pause => { pause.Enabled = true; pause.HiddenDelay = TimeSpan.FromSeconds(30); }); });
当页面处于隐藏状态并达到指定时间以后,Circuit 可以进入暂停状态,释放服务器资源;用户重新回来后再恢复。
不过微软也没有粗暴地说“Tab 隐藏 30 秒直接冻死”。
如果页面中存在未保存文本、正在播放的未静音媒体、Picture-in-Picture、Web Lock,或者还有 JS Interop、Stream Transfer、Render 等活动,自动暂停可能会被延后。
也就是说,它想解决的是:真正闲置的 Circuit。
而不是把用户正在进行的工作强行暂停。
对于拥有大量同时在线用户的 Blazor Server 应用,我觉得这个方向相当重要。
Blazor SSR 现在甚至可以缓存一块组件树的 HTML
另一个有意思的功能叫 CacheView。
以前做 SSR 时,一个组件区域每次 Request 都重新实例化、执行、渲染。
现在可以:
<CacheView ExpiresAfter="TimeSpan.FromMinutes(10)" VaryByRoute="productId" VaryByQuery="page,pageSize" VaryByCulture="true"> <ExpensiveProductSummary ProductId="productId" /></CacheView>
当缓存命中以后,CacheView 内部的子组件甚至不会重新实例化和渲染,而是直接重放之前捕获的 HTML。
这个思路其实和 Web 开发中非常成熟的:
Response CacheFragment CachePartial Cache
很接近。
只不过现在进入了 Blazor 组件模型。
对于商品详情页、CMS 内容、导航区域以及高成本 SSR Component,这个功能可能比单纯优化组件生命周期有价值得多。
Validation 也在继续收口
.NET 11 前几个 Preview 一直在改新的 Validation 体系。
Preview 7 做了一件很重要的事情:本地化能力直接进入 Microsoft.Extensions.Validation,不再需要单独的 Preview Localization 包。
只要 DI 中存在 IStringLocalizerFactory,Validation 就可以走本地化流程。
例如:
builder.Services.AddLocalization();builder.Services.AddValidation();Model:[ValidatableType]public class Customer{ [Display(Name = "CustomerName")] [Required( ErrorMessage = "NameRequired")] public string? Name { get; set; }}
这里 CustomerName 和 NameRequired 就可以作为资源 Key 参与本地化。
这种变化其实透露出一个趋势:
Validation 正在从 MVC 的附属能力逐渐变成整个 .NET Extensions 体系里的基础设施。
SSE 和 OpenAPI 也终于开始真正互相认识了
现在 AI Streaming、实时日志以及 Agent 输出越来越多地使用 Server-Sent Events。
ASP.NET Core 本身已经可以返回 SSE:
app.MapGet("/messages/stream",(CancellationToken cancellationToken) => TypedResults.ServerSentEvents( GetMessagesAsync( cancellationToken)));
Preview 7 的变化是,如果 Endpoint 返回 SseItem<T>,生成 OpenAPI 3.2 文档时可以使用 itemSchema 来描述每一条事件的数据类型,而不是简单地把整个 text/event-stream 当成字符串。
这对 SDK Generator、API Explorer 和 AI API 工具来说挺重要。
因为:这是一个 text/event-stream和:这是一个 event stream,其中每一条 event 都是 TodoDto完全不是一个信息量级。
Windows Forms 也没躺平:.NET 11 开始换新的视觉体系
最后聊聊 WinForms。
每次新 .NET 发布,很多人第一反应都是:WinForms 还有更新?答案不只是有,而且 Preview 7 的改动还挺实际。
现在 WinForms 加入新的 VisualStylesMode,可以明确选择 Classic、Disabled、Net11 或 Latest。
例如:
using System.Windows.Forms;Application.SetDefaultVisualStylesMode( VisualStylesMode.Net11);var form = new Form{ Text = ".NET 11 WinForms", VisualStylesMode = VisualStylesMode.Latest};Application.Run(form);
新的渲染模式会逐步覆盖 Button、CheckBox、RadioButton、GroupBox、TextBox、RichTextBox 等常用控件,TextBox / RichTextBox 还开始支持与新视觉样式配合的 Padding 和圆角化表现。
与此同时,应用还可以监听系统视觉设置的变化。
比如 Windows Accent Color 变化:
Application.SystemVisualSettingsChanged += (_, e) => { if ((e.Changed & SystemVisualSettingsCategories.AccentColor) != 0) { Console.WriteLine( e.NewSettings.AccentColor); } };
文本缩放比例改变也可以收到通知。
这意味着 WinForms 应用以后对 Windows 系统主题和辅助功能的响应可以做得更自然。
WinForms 还有一个非常实用的性能 API
如果你曾经往 ListBox、TreeView、ListView 里面一次塞几千条数据,应该见过那种:界面疯狂重绘、布局不停计算,最后窗口一卡一卡的。
过去往往要组合:
BeginUpdateEndUpdateSuspendLayoutResumeLayout
Preview 7 把这类需求统一成了一个非常符合现代 C# 写法的 API:
using var _ =listBox.SuspendPainting( LayoutSuspendTraversal.Target);for (int i = 0; i < 10_000; i++){ listBox.Items.Add( $"Item {i}");}
离开 using Scope 时恢复 Layout 和 Painting。
完整一点就是:
using System.Windows.Forms;var listBox = new ListBox();using ( listBox.SuspendPainting( LayoutSuspendTraversal.Target)){ for (int i = 0; i < 10_000; i++) { listBox.Items.Add( $"Item {i}"); }}
这属于我很喜欢的 API 设计。
功能本身不新鲜,但:
using (...){}
天然保证恢复动作能够发生,比手工 Begin / End 更不容易写错。
所以,.NET 11 Preview 7 到底在做什么?
把这六份 Release Notes 放在一起以后,我觉得 .NET 11 的方向已经比较明显了。
它不是那种:我们今年再增加 100 个 API。
而是在把一些已经使用很多年的核心链路重新往下优化。
C# 这边,union、closed、穷尽性分析正在加强类型系统表达能力。
Runtime 这边,JIT 已经开始进一步理解 async/await、Task 和 ValueTask,而不只是接受编译器生成的状态机。
SDK 这边更直接:NativeAOT CLI 和 MSBuild Server 默认开启,目标就是让每天执行几十次的 dotnet 命令少等一点。
Libraries 则在补一些长期缺失的基础设施:IEEE 754 Decimal、泛型 Complex、部分数字解析、Request Compression、完整 DNS Record 查询、加密 ZIP。
ASP.NET Core 的重点之一则从“怎么让 Blazor 能工作”,逐渐变成“怎么让大规模 Blazor 应用更省资源、更容易缓存、更容易诊断”。
甚至连 WinForms,都开始认真处理新视觉样式、系统设置变化和批量 UI 更新。
所以如果一定要给 .NET 11 Preview 7 找一个关键词,我觉得不是“新功能”。
而是:把基础设施继续往下做。
很多东西第一次看到甚至不会让人觉得惊艳。
break outer?
一个新的 ZIP API?
MSBuild Server 默认打开?
后台 Tab 暂停 Circuit?
这些看起来都是小东西。
但真正一个平台用到十年、二十年以后,影响开发体验的往往恰恰就是这些东西。
语法少绕一点。
CLI 少等 300 毫秒。
异步少一次 Allocation。
服务器少维持几万个没人在看的 Circuit。
批量加控件时少刷新几千次。
这些改进单独拿出来都不算“革命”,但堆在一起,就是一个成熟开发平台继续演进的方式。
而从 Preview 7 现在的状态来看,.NET 11 已经越来越接近一版值得认真关注的基础设施升级了。
声明:来自硅基-桂迹,仅代表创作者观点。链接:https://eyangzhen.com/8968.html