原文 | Klaus Loeffelmann
翻译 | 郑子铭
如果您从未看过电影《分析这一点》,下面是简短的介绍:假设一个纽约家族的成员有可疑的习惯,他决定认真考虑接受治疗以改善他的精神状态。在比利·克里斯托和罗伯特·德尼罗的推动下,剧情一定会很有趣。虽然《分析这一点!》讽刺性地处理了被漫画化的 MOB 世界的问题,但使用正确的分析工具找到问题的根源在任何地方都至关重要。在任务关键型 LOB-App 世界中更是如此。
进入新的 WinForms Roslyn Analyzers,这是 WinForms 应用程序的特定领域“顾问”。借助 .NET 9,我们推出了这些分析器,以帮助您的代码解决其潜在问题 — 无论是错误行为、可疑模式还是改进机会。
Roslyn 分析器到底是什么?
Roslyn 分析器是 Roslyn 编译器平台的核心部分,可在后台无缝工作,在您编写代码时对其进行分析。您可能已经使用它们多年却没有意识到这一点。Visual Studio 中的许多功能(如代码修复、重构建议和错误诊断)都依赖于分析器或 CodeFixes,甚至只是它们本身,以增强您的开发过程。它们已成为现代开发不可或缺的一部分,以至于我们常常将它们视为理所当然,只是“事物的工作原理”。
最酷的是:这个基于 Roslyn 的编译器平台不是黑盒子。它们提供了极其丰富的 API,不仅 Microsoft 的 Visual Studio IDE 或编译器团队可以创建分析器。每个人都可以。这就是 WinForms 选择这项技术来改善 WinForms 编码体验的原因。
这只是开始 — 未来还有更多
借助 .NET 9,我们为 WinForms 专用分析器奠定了基础架构,并引入了第一组规则。这些分析器旨在解决安全性、稳定性和生产力等关键领域。虽然这只是开始,但我们致力于在未来版本中扩大其范围,并推出更多规则和功能。
那么,让我们真正了解一下我们为 .NET 9 引入的第一组分析器:
选择正确的 InvokeAsync 重载的指南
在 .NET 9 中,我们为 WinForms 引入了一系列新的异步 API。这篇博文详细介绍了新的 WinForms 异步功能。这是我们认为 WinForms Analyzer 可以在防止异步代码出现问题方面提供很大帮助的首批领域之一。
使用新的 Control.InvokeAsync
API 时面临的一个挑战是从以下选项中选择正确的重载:
public async Task InvokeAsync(Action callback, CancellationToken cancellationToken = default)
public async Task<T> InvokeAsync<T>(Func<T> callback, CancellationToken cancellationToken = default)
public async Task InvokeAsync(Func<CancellationToken, ValueTask> callback, CancellationToken cancellationToken = default)
public async Task<T> InvokeAsync<T>(Func<CancellationToken, ValueTask<T>> callback, CancellationToken cancellationToken = default)
每个重载都支持同步和异步方法的不同组合,有或没有返回值。链接的博客文章提供了有关这些 API 的全面背景信息。
但是,选择错误的重载可能会导致应用程序中的代码路径不稳定。为了缓解这种情况,我们实施了一个分析器,以帮助开发人员根据其特定用例选择最合适的 InvokeAsync
重载。
潜在的问题如下:InvokeAsync
可以异步调用同步和异步方法。对于异步方法,您可能会传递 Func<Task>
,并期望它被等待,但事实并非如此。Func<T>
专门用于异步调用同步调用方法 - 其中 Func<Task>
只是一个不幸的特殊情况。
换句话说,问题出现是因为 InvokeAsync
可以接受任何 Func<T>
。但只有 Func<CancellationToken, ValueTask>
才能被 API 正确等待。如果您传递没有正确签名的 Func<Task>
(不接受 CancellationToken
并返回 ValueTask
的签名),它将不会被等待。这会导致“发射后不管”的情况,其中函数内的异常无法正确处理。如果这样的函数随后抛出异常,它可能会损坏数据,甚至导致整个应用程序崩溃。
看看以下场景:
private async void StartButtonClick(object sender, EventArgs e)
{_btnStartStopWatch.Text = _btnStartStopWatch.Text != "Stop" ? "Stop" : "Start";await Task.Run(async () =>{while (true){await this.InvokeAsync(UpdateUiAsync);}});// ****// The actual UI update method// ****async Task UpdateUiAsync(){_lblStopWatch.Text = $"{DateTime.Now:HH:mm:ss - fff}";await Task.Delay(20);}
}
这是一个典型的场景,InvokeAsync 的重载被意外使用,而它本应返回任务以外的内容。但分析器指出:
因此,通过此通知,我们也清楚地认识到我们实际上需要引入一个取消令牌,以便我们可以正常结束正在运行的任务,无论是当用户再次单击按钮时,还是当 Form 实际关闭时(这更重要)。否则,任务将继续运行,而 Form 将被处置。这将导致崩溃:
private async void ButtonClick(object sender, EventArgs e){if (_stopWatchToken.CanBeCanceled){_btnStartStopWatch.Text = "Start";_stopWatchTokenSource.Cancel();_stopWatchTokenSource.Dispose();_stopWatchTokenSource = new CancellationTokenSource();_stopWatchToken = CancellationToken.None;return;}_stopWatchToken = _stopWatchTokenSource.Token;_btnStartStopWatch.Text = "Stop";await Task.Run(async () =>{while (true){try{await this.InvokeAsync(UpdateUiAsync, _stopWatchToken);}catch (TaskCanceledException){break;}}});// ****// The actual UI update method// ****async ValueTask UpdateUiAsync(CancellationToken cancellation){_lblStopWatch.Text = $"{DateTime.Now:HH:mm:ss - fff}";await Task.Delay(20, cancellation);}}protected override void OnFormClosing(FormClosingEventArgs e){base.OnFormClosing(e);_stopWatchTokenSource.Cancel();}
分析器通过检测 InvokeAsync
的不兼容用法并指导您选择正确的重载来解决此问题。这可确保异步代码中行为稳定、可预测且异常处理正确。
防止设计时业务数据泄露
在开发自定义控件或从 UserControl
派生的业务控制逻辑类时,通常使用属性来管理其行为和外观。然而,如果在设计时无意中设置了这些属性,就会出现一个常见问题。这通常是因为在设计阶段没有适当的机制来防范此类情况。
如果未正确配置这些属性以控制其代码序列化行为,则设计时设置的敏感数据可能会无意中泄漏到生成的代码中。此类泄漏可能导致:
- 敏感数据暴露在源代码中,可能发布在 GitHub 等平台上。
- 设计时数据嵌入到资源文件中,要么是因为缺少相关属性类型的必要 TypeConverters,要么是因为表单已本地化。
这两种情况都会对应用程序的完整性和安全性造成重大风险。
此外,我们力求尽可能避免资源序列化。在 .NET 9 中,由于安全性和可维护性问题,二进制格式化程序和相关 API 已被淘汰。这使得仔细控制哪些数据被序列化以及如何序列化变得更加重要。
二进制格式化程序过去用于序列化对象,但它存在许多安全漏洞,使其不适合现代应用程序。在 .NET 9 中,我们完全消除了这个序列化程序,以减少攻击面并提高应用程序的可靠性。任何对资源序列化的依赖都有可能再次引入这些风险,因此采用更安全的做法至关重要。
为了帮助您(开发人员)解决这个问题,我们引入了一个 WinForms 特定的分析器。当缺少以下所有用于控制属性的 CodeDOM 序列化过程的机制时,此分析器将激活:
SerializationVisibilityAttribute
:此属性控制 CodeDOM 序列化程序应如何(或是否)序列化属性的内容。- 属性不是只读的,因为 CodeDOM 序列化程序默认忽略只读属性。
DefaultValueAttribute
:此属性定义属性的默认值。如果应用,CodeDOM 序列化程序仅在设计时的当前值与默认值不同时序列化属性。- 未实现相应的
private bool ShouldSerialize<PropertyName>()
方法。在设计(序列化)时调用此方法来确定是否应序列化属性的内容。
通过确保至少存在其中一种机制,您可以避免意外的序列化行为,并确保在设计时 CodeDOM 序列化过程中正确处理您的属性。
但是……这个分析器破坏了我的整个解决方案!
假设您在 .NET 8 中开发了一个特定于域的 UserControl
,如上面的屏幕截图所示。现在,您正在将项目重新定位到 .NET 9。那么,显然,在那一刻,分析器启动了,您可能会看到类似这样的内容:
与之前讨论的 Async Analyzer 不同,此分析器附带了 Roslyn CodeFix。如果您想通过指示 CodeDOM 序列化程序无条件地永不序列化属性内容来解决这个问题,您可以使用 CodeFix 进行必要的更改:
如您所见,您甚至可以在整个文档中一次性修复它们。在大多数情况下,这已经是正确的做法:分析器在每个标记属性的顶部添加 SerializationVisibilityAttribute
,确保它不会被无意中序列化,这正是我们想要的:
...[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]public string NameText{get => textBoxName.Text;set => textBoxName.Text = value;}[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]public string EmailText{get => textBoxEmail.Text;set => textBoxEmail.Text = value;}[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]public string PhoneText{get => textBoxPhone.Text;set => textBoxPhone.Text = value;}...
Copilot 来救场了!
还有一种更有效的方法来处理属性的必要编辑修改。您可能想问自己的问题是:如果根本没有应用任何属性来控制属性的某些方面,那么不仅要确保正确的序列化指导,还要应用其他设计时属性是否有意义?
但话又说回来,所需的努力会更大吗?
那么,如果我们利用 Copilot 来修改所有在设计时真正有用的相关属性,如 DescriptionAttribute
或 CategoryAttribute
,会怎么样?让我们试一试,如下所示:
根据您为 Copilot 选择的语言模型,您应该看到这样的结果:我们不仅解决了分析器指出的问题,而且 Copilot 还负责添加在上下文中有意义的其余属性。
Copilot 向您显示它想要添加的代码,您只需单击一下鼠标即可合并建议的更改。
而且这些问题肯定不是 Copilot 能够帮助您实现现有 WinForms 应用程序现代化的唯一领域。
但是,如果分析器在整个解决方案中标记了数百个问题,请不要惊慌!还有更多选项可以在代码文件、项目甚至解决方案级别配置分析器的严重性:
根据范围抑制分析器
首先,您可以选择抑制不同范围内的分析器:
- 在源代码中:此选项在标记代码周围的源文件中直接插入 #pragma warning disable 指令。此方法适用于本地化、一次性抑制,其中分析器警告是不必要的或无关紧要的。例如:
#pragma warning disable WFO1000
public string SomeProperty { get; set; }
#pragma warning restore WFO1000
- 在抑制文件中:这会将抑制添加到项目中名为 GlobalSuppressions.cs 的文件中。此文件中的抑制作用范围是全局的,范围是程序集或命名空间,因此对于较大规模的抑制来说,这是一个不错的选择。例如:
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("WinForms.Analyzers","WFO1000",Justification = "This property is intentionally serialized.")]
- 通过属性在源代码中:这会将抑制属性直接应用于特定代码元素,例如类或属性。如果您希望抑制仍是源代码文档的一部分,这是一个不错的选择。例如:
[System.Diagnostics.CodeAnalysis.SuppressMessage("WinForms.Analyzers","WFO1000",Justification = "This property is handled manually.")]
public string SomeProperty { get; set; }
在 .editorconfig 中配置分析器严重性
要为您的项目或解决方案集中配置分析器严重性,您可以使用 .editorconfig 文件。此文件允许您为特定分析器定义规则,包括其严重性级别,例如无、建议、警告或错误。例如,要更改 WFO1000 分析器的严重性:
# Configure the severity for the WFO1000 analyzer
dotnet_diagnostic.WFO1000.severity = warning
使用 .editorconfig 文件进行目录特定设置
.editorconfig 文件的强大功能之一是它们能够控制解决方案不同部分的设置。通过将 .editorconfig 文件放置在解决方案内的不同目录中,您可以将设置仅应用于特定项目、文件夹或文件。配置按层次结构应用,这意味着子目录的 .editorconfig 文件中的设置可以覆盖父目录中的设置。
例如:
- 根级 .editorconfig:将通用 .editorconfig 文件放置在解决方案根目录中,以定义应用于整个解决方案的默认设置。
- 项目特定 .editorconfig:将另一个 .editorconfig 文件放置在特定项目的目录中,以便在从根目录继承设置的同时为该项目应用不同的规则。
- 文件夹特定 .editorconfig:如果某些文件夹(例如,测试项目、遗留代码)需要唯一设置,您可以向这些文件夹添加 .editorconfig 文件以覆盖继承的配置。
/solution-root├── .editorconfig (applies to all projects)├── ProjectA/│ ├── .editorconfig (overrides root settings for ProjectA)│ └── CodeFile.cs├── ProjectB/│ ├── .editorconfig (specific to ProjectB)│ └── CodeFile.cs├── Shared/│ ├── .editorconfig (applies to shared utilities)│ └── Utility.cs
在此布局中,根目录中的 .editorconfig 将常规设置应用于解决方案中的所有文件。ProjectA 中的 .editorconfig 将应用特定于 ProjectA 的附加或覆盖规则。同样,ProjectB 和共享目录可以定义其唯一设置。
- 目录特定 .editorconfig 文件的用例测试项目:禁用或降低某些测试项目的某些分析器的严重性,因为某些规则可能不适用。
# In TestProject/.editorconfig
dotnet_diagnostic.WFO1000.severity = none
Legacy Code: Suppress analyzers entirely or reduce their impact for legacy codebases to avoid unnecessary noise.
# In LegacyCode/.editorconfig
dotnet_diagnostic.WFO1000.severity = suggestion
Experimental Features: Use more lenient settings for projects under active development while enforcing stricter rules for production-ready code.
通过策略性地放置 .editorconfig 文件,您可以对分析器的行为和编码约定进行细粒度控制,从而更轻松地管理具有各种要求的大型解决方案。请记住,此分析器的目标是引导您编写更安全、更易于维护的代码,但由您决定在项目中解决这些问题的最佳速度和优先级。
如您所见:.editorconfig 文件或一组经过深思熟虑的此类文件提供了一种集中且一致的方式来管理整个项目或团队的分析器行为。
有关更多详细信息,请参阅 .editorconfig 文档。
所以,我对 WinForms 分析器有很好的想法 - 我可以贡献吗?
当然!WinForms 团队和社区一直在寻找改善开发人员体验的想法。如果您对新分析器或现有分析器的增强功能有建议,您可以通过以下方式做出贡献:
- 打开问题:前往 WinForms GitHub 存储库并打开一个问题来描述您的想法。尽可能详细地解释您的分析器将解决的问题及其工作原理。
- 加入讨论:在 GitHub 或其他论坛上与 WinForms 社区互动。其他开发人员的反馈可以帮助您完善您的想法。
- 贡献代码:如果您熟悉 .NET Roslyn 分析器框架,请考虑实施您的想法并向存储库提交拉取请求。团队积极审查和合并社区贡献。
- 测试和迭代:在提交拉取请求之前,请使用真实场景彻底测试您的分析器,以确保它按预期工作并且不会引入误报。
为生态系统做出贡献不仅可以帮助他人,还可以加深您对 WinForms 开发和 .NET 平台的理解。
最后的话
分析器是帮助开发人员编写更好、更可靠和安全的代码的强大工具。虽然它们最初看起来可能具有侵入性——尤其是当它们标记许多问题时——但它们可以充当安全网,指导您避免常见的陷阱并采用最佳实践。
新的 WinForms 专用分析器是我们持续努力的一部分,旨在使平台现代化和保护,同时保持其简单性和易用性。无论您是在处理旧应用程序还是构建新应用程序,这些工具都旨在让您的开发体验更加顺畅。
如果您遇到问题或有改进想法,我们很乐意听取您的意见!WinForms 因其热情而专注的社区而蓬勃发展了数十年,您的贡献确保它继续发展并在当今的发展格局中保持相关性。
祝您编码愉快!
原文链接
WinForms: Analyze This (Me in Visual Basic)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com)