借助.net功能,开发人员可以高效的编写可靠的性能代码
使用 Async 和 Await 的异步编程
1、异步代码(在一件事情完成之前,你可以同时去做另一件事)
举一个简单的例子:
做早餐的步骤{
- 倒一杯咖啡。
- 加热平底锅,然后煎两个鸡蛋。
- 煎三片培根。
- 烤两片面包。
- 在烤面包上加黄油和果酱。
- 倒一杯橙汁。
}
如果你有烹饪经验,你会按照异步方式执行这些操作:。
你会加热平底锅(同时备煎蛋,再准备培根的工作),然后你可以把面包放进烤箱(同时再去煎鸡蛋和煎培根)
同时做早餐是一个非并行异步工作的例子
对于并行算法而言,你则需要多名厨师(或线程)。 一名厨师煎鸡蛋,一名厨师煎培根,依次类推。 每名厨师将仅专注于一项任务。 每名厨师(或线程)都将被阻止同步等待翻动培根或弹出面包。
下面我们考虑编写C#的语句指令
同步准备的早餐大约花费了 30 分钟,因为总耗时是每个任务耗时的总和
计算机不会按人类的方式来解释这些指令。 计算机将阻塞每条语句,直到工作完成,然后再继续运行下一条语句。 这将创造出令人不满意的早餐。 后续任务直到早前任务完成后才会启动。 这样做早餐花费的时间要长得多,有些食物在上桌之前就已经凉了。
如果你希望计算机异步执行上述指令,则必须编写异步代码。
这些问题对即将编写的程序而言至关重要。 编写客户端程序时,你希望 UI 能够响应用户输入。 从 Web 下载数据时,你的应用程序不应让手机出现卡顿。 编写服务器程序时,你不希望线程受到阻塞。 这些线程可以用于处理其他请求。 存在异步替代项的情况下使用同步代码会增加你进行扩展的成本。 你需要为这些受阻线程付费。
成功的现代应用程序需要异步代码。 在没有语言支持的情况下,编写异步代码需要回调、完成事件,或其他掩盖代码原始意图的方法。 同步代码的优点是,它的分步操作使其易于扫描和理解。 传统的异步模型迫使你侧重于代码的异步性质,而不是代码的基本操作。
不要阻塞,而要 await
上述代码演示了不正确的实践:构造同步代码来执行异步操作。
此代码将阻止执行这段代码的线程执行任何其他操作。 在任何任务进行过程中,此代码也不会被中断。 就如同你将面包放进烤面包机后盯着此烤面包机一样。 你会无视任何跟你说话的人,直到面包弹出。
我们首先更新此代码,使线程在任务运行时不会阻塞。
await
关键字提供了一种非阻塞方式来启动任务,然后在此任务完成时继续执行。 “做早餐”代码的简单异步版本类似于以下片段
在煎鸡蛋或培根时,此代码不会阻塞。 不过,此代码也不会启动任何其他任务。 你还是会将面包放进烤面包机里,然后盯着烤面包机直到面包弹出。 但至少,你会回应任何想引起你注意的人。
现在,在等待任何尚未完成的已启动任务时,处理早餐的线程将不会被阻塞。 对于某些应用程序而言,此更改是必需的。 仅凭借此更改,GUI 应用程序仍然会响应用户。 然而,对于此方案而言,你需要更多的内容。 你不希望每个组件任务都按顺序执行。 最好首先启动每个组件任务,然后再等待之前任务的完成。
同时启动任务
在许多方案中,你希望立即启动若干独立的任务。 然后,在每个任务完成时,你可以继续进行已准备的其他工作。 在早餐类比中,这就是更快完成做早餐的方法。 你也几乎将在同一时间完成所有工作。 你将吃到一顿热气腾腾的早餐。
System.Threading.Tasks.Task 和相关类型是可以用于推断正在进行中的任务的类。 这使你能够编写更类似于做早餐方式的代码。 你可以同时开始煎鸡蛋、培根和烤面包。 由于每个任务都需要操作,所以你会将注意力转移到那个任务上,进行下一个操作,然后等待其他需要你注意的事情。
启动一项任务并等待表示运行的 Task 对象。 你将首先 await
每项任务,然后再处理它的结果。
让我们对早餐代码进行这些更改。 第一步是存储任务以便在这些任务启动时进行操作,而不是等待:
接下来,可以在提供早餐之前将用于处理培根和鸡蛋的 await
语句移动到此方法的末尾:
异步准备的早餐大约花费了 20 分钟,由于一些任务并发运行,因此节约了时间。
上述代码效果更好。 你可以一次启动所有的异步任务。 你仅在需要结果时才会等待每项任务。
与任务组合
除了吐司外,你准备好了做早餐的所有材料。 吐司制作由异步操作(烤面包)和同步操作(添加黄油和果酱)组成。 更新此代码说明了一个重要的概念:
上述代码展示了可以使用 Task 或 Task<TResult> 对象来保存运行中的任务。 你首先需要 await
每项任务,然后再使用它的结果。 下一步是创建表示其他工作组合的方式。 在提供早餐之前,你希望等待表示先烤面包再添加黄油和果酱的任务完成。 你可以使用以下代码表示此工作:
上述方式的签名中具有 async
修饰符。 它会向编译器发出信号,说明此方法包含 await
语句;也包含异步操作。 此方法表示先烤面包,然后再添加黄油和果酱的任务。 此方法返回表示这三个操作的组合的 Task<TResult>。 主要代码块现在变成了:
上述更改说明了使用异步代码的一项重要技术。 你可以通过将操作分离到一个返回任务的新方法中来组合任务。 可以选择等待此任务的时间。 可以同时启动其他任务。
异步异常
异步方法会引发异常,就像对应的同步方法一样。 对异常和错误处理的异步支持通常与异步支持追求相同的目标:你应该编写读起来像一系列同步语句的代码。 当任务无法成功完成时,它们将引发异常。 当启动的任务为 awaited
时,客户端代码可捕获这些异常。 例如,假设烤面包机在烤面包时着火了。 可通过修改 ToastBreadAsync
方法来模拟这种情况,以匹配以下代码:
在编译前面的代码时,你将收到一个关于无法访问的代码的警告。 这是故意的,因为一旦烤面包机着火,操作就不会正常进行。
执行这些更改后,运行应用程序,输出将类似于以下文本:
请注意,从烤面包机烧着到发现异常,有相当多的任务要完成。 当异步运行的任务引发异常时,该任务出错。 Task 对象包含 Task.Exception 属性中引发的异常。 出错的任务在等待时引发异常。
需要理解两个重要机制:异常在出错的任务中的存储方式,以及在代码等待出错的任务时解包并重新引发异常的方式。
当异步运行的代码引发异常时,该异常存储在 Task
中。 Task.Exception 属性为 System.AggregateException,因为异步工作期间可能会引发多个异常。 引发的任何异常都将添加到 AggregateException.InnerExceptions 集合中。 如果该 Exception
属性为 NULL,则将创建一个新的 AggregateException
且引发的异常是该集合中的第一项。
对于出错的任务,最常见的情况是 Exception
属性只包含一个异常。 当代码 awaits
出错的任务时,将重新引发 AggregateException.InnerExceptions 集合中的第一个异常。 因此,此示例的输出显示 InvalidOperationException
而不是 AggregateException
。 提取第一个内部异常使得使用异步方法与使用其对应的同步方法尽可能相似。 当你的场景可能生成多个异常时,可在代码中检查 Exception
属性。
高效地等待任务
可以通过使用 Task
类的方法改进上述代码末尾的一系列 await
语句。 其中一个 API 是 WhenAll,它将返回一个其参数列表中的所有任务都已完成时才完成的 Task,如以下代码中所示:
另一种选择是使用 WhenAny,它将返回一个当其参数完成时才完成的 Task<Task>
。 你可以等待返回的任务,了解它已经完成了。 以下代码展示了可以如何使用 WhenAny 等待第一个任务完成,然后再处理其结果。 处理已完成任务的结果之后,可以从传递给 WhenAny
的任务列表中删除此已完成的任务。
进行所有这些更改之后,代码的最终版本将如下所示:
异步准备的早餐的最终版本大约花费了 15 分钟,因为一些任务并行运行,并且代码同时监视多个任务,只在需要时才执行操作。
利用特性扩展元数据
2、属性
公共语言运行时使你能够添加类似于关键字的描述性声明(称为特性),以便批注编程元素(如类型、字段、方法和属性)。
编译运行时的代码时,它将被转换为 Microsoft 中间语言 (MSIL),并和编译器生成的元数据一起放置在可移植可执行 (PE) 文件内。
特性使你能够将额外的描述性信息放到可使用运行时反射服务提取的元数据中。
当你声明派生自 System.Attribute的特殊类的实例时,编译器会创建特性。
特性描述如何将数据序列化、指定用于强制安全性的特征并限制通过实时 (JIT) 编译器进行优化,从而使代码易于调试。 特性还可记录文件的名称或代码的作者,或控制窗体开发过程中控件和成员的可见性。
3、反射 (C#)
反射提供描述程序集、模块和类型的对象(Type 类型)。
可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问器字段和属性。
如果代码中使用了特性,可以利用反射来访问它们。
.NET 源代码分析概述
4、代码分析器
.NET Compiler Platform (Roslyn) 分析器会检查 C# 或 Visual Basic 代码的代码质量和样式问题。
.NET Compiler Platform (Roslyn) 分析器会检查 C# 或 Visual Basic 代码的代码质量和样式问题。 从 .NET 5 开始,这些分析器包含在 .NET SDK 中,无需单独安装。
如果项目面向不同的 .NET 实现(例如 .NET Core、.NET Standard 或 .NET Framework),则必须通过将 EnableNETAnalyzers 属性设置为 true
以手动启用代码分析。
代码质量分析
代码质量分析(“CAxxxx”)规则检查 C# 或 Visual Basic 代码的安全性、性能、设计及其他问题。 分析功能针对面向 .NET 5 或更高版本的项目默认启用。 可通过将 EnableNETAnalyzers 属性设置为 true
,在面向 .NET 早期版本的项目上启用代码分析。 你也可通过将 EnableNETAnalyzers
设置为 false
,对项目禁用代码分析
启用其他规则
分析模式指预定义的代码分析配置,在此配置下,未启用任何规则、启用某些规则或启用所有规则。 在默认分析模式下,只有少量规则作为生成警告启用。 可通过在项目文件中设置 <AnalysisMode> 属性来更改项目的分析模式。 允许的值为:
None
Default
Minimum
Recommended
All
从 .NET 6 开始,可省略 <AnalysisMode> 来支持 <AnalysisLevel> 属性的复合值。 例如,以下值为最新版本实现推荐的一组规则:<AnalysisLevel>latest-Recommended</AnalysisLevel>
。 有关详细信息,请参阅 AnalysisLevel。
若要查找每个可用规则的默认严重性以及了解规则是否在默认分析模式下启用,请参阅规则列表。
视警告为错误
如果在生成项目时使用 -warnaserror
标志,则所有代码分析警告也会被视为错误。 如果不希望在出现 -warnaserror
时将代码质量警告 (CAxxxx) 视为错误,可在项目文件中将 CodeAnalysisTreatWarningsAsErrors
MSBuild 属性设置为 false
。
你仍会看到任何代码分析警告,但它们不会中断生成。
最新更新
默认情况下,在升级到较新版本的 .NET SDK 时,你将获得最新的代码分析规则和默认规则严重性。 如果你不希望出现此行为(例如,如果你想要确保未启用或禁用任何新规则),可通过以下方式之一来替代此行为:
有关代码样式分析规则的完整列表,请参阅代码样式规则。
-
将
AnalysisLevel
MSBuild 属性设置为特定值,以将警告锁定到相应的集。 在升级到较新的 SDK 时,你仍会获得针对这些警告的 bug 修补程序,但系统不会启用新的警告,也不会禁用现有的警告。 例如,若要将规则集锁定为随 .NET SDK 5.0 版本一起提供的规则集,请向项目文件添加以下条目。 -
代码样式分析
-
通过代码样式分析(“IDExxxx”)规则,可在代码库中定义和维护一致的代码样式。 默认的启用设置为:
-
命令行生成:默认情况下,对命令行生成上的所有 .NET 项目禁用代码样式分析。
从 .NET 5 开始,无论是在命令行还是在 Visual Studio 内,你都可以在生成时启用代码样式分析。 代码样式冲突显示为带有“IDE”前缀的警告或错误。 这使你能够在生成时强制执行一致的代码样式。
-
Visual Studio:默认情况下,代码样式分析作为代码重构快速操作对 Visual Studio 中的所有 .NET 项目启用。
生成时启用
通过 .NET 5 SDK 及更高版本,可在从命令行和 Visual Studio 生成时启用代码样式分析。 (然而,出于性能方面的原因,一些代码样式规则仍仅适用于 Visual Studio IDE。)
执行以下步骤,在生成时启用代码样式分析:
-
将 MSBuild 属性 EnforceCodeStyleInBuild 设置为
true
。 -
在 .editorconfig 文件中,配置你希望在生成时作为警告或错误运行的每个“IDE”代码样式规则。 例如:
-
或者,可将整个类别默认配置为警告或错误,然后选择性地禁用该类别中你不希望在生成时运行的规则。 例如:
-
抑制警告
一种抑制规则冲突的方法是在 EditorConfig 文件中将该规则 ID 的严重性选项设置为 none
。 例如: