为什么应该使用枚举源生成器?
NetEscapades.EnumGenerators 是我在 .NET 6 中引入增量生成器支持后创建的首批源生成器之一。创建这个包是为了解决使用枚举时的一个恼人特性:某些操作出乎意料地慢。
需要注意的是,虽然这在历史上是事实,但这情况并不一定会永远持续。实际上,.NET 8+ 在运行时中对枚举处理进行了一系列改进。
例如,假设你有以下枚举:
|
|
在某些时候,你想打印一个 Color 变量的名称,于是你创建了这个辅助方法:
|
|
虽然这看起来应该很快,但实际上并非如此。NetEscapades.EnumGenerators 通过自动生成一个快速的实现来工作。它会生成一个类似这样的 ToStringFast() 方法:
|
|
这个简单的 switch 语句检查 Colour 的每个已知值,并使用 nameof 返回枚举的文本表示。如果是未知值,则回退到内置的 ToString() 实现,以确保正确处.理未知值(例如,PrintColour((Colour)123) 是有效的 C# 代码)。如果我们使用 BenchmarkDotNet 比较这两种实现对于一个已知颜色的性能,你可以看到 ToStringFast() 实现快了多少:
| 方法 | 框架 | 平均时间 | 误差 | 标准差 | 比率 | Gen 0 | 分配内存 |
|---|---|---|---|---|---|---|---|
| ToString | net48 | 578.276 ns | 3.3109 ns | 3.0970 ns | 1.00 | 0.0458 | 96 B |
| ToStringFast | net48 | 3.091 ns | 0.0567 ns | 0.0443 ns | 0.01 | - | - |
| ToString | net6.0 | 17.985 ns | 0.1230 ns | 0.1151 ns | 1.00 | 0.0115 | 24 B |
| ToStringFast | net6.0 | 0.121 ns | 0.0225 ns | 0.0199 ns | 0.01 | - | - |
这些数字现在显然已经相当旧了,但整体模式没有改变:.NET 比 .NET Framework 快得多,而 ToStringFast() 实现比内置的 ToString() 快得多。显然,你的情况可能有所不同,结果将取决于你使用的具体枚举,但通常来说,使用源生成器应该能免费带来性能提升。
基础部分已经介绍完毕,现在让我们看看有什么新内容。
1.0.0-beta.16 版本中的更新
NetEscapades.EnumGenerators 的 1.0.0-beta16 版本于 11 月 4 日发布到 nuget.org,包含了许多生活质量改进和错误修复。我将在下面更详细地描述每个更新,但它们属于以下三类之一:
- 对诸如
[Display]和[Description]等“附加元数据属性”工作方式的重构。 - 额外的分析器,以确保
[EnumExtensions]被正确使用。 - 针对边缘情况的错误修复。
让我们先来看看更新的元数据属性支持。
更新的元数据属性和 [EnumMember] 支持
很长时间以来,你一直能够使用应用于枚举成员的 [Display] 或 [Description] 属性来自定义 ToStringFast 或 Parse 与库的交互方式。例如,如果你有以下枚举:
|
|
那么会生成三种不同的 ToString 方法:ToStringFast() 的两个重载和 ToStringFastWithMetadata():
|
|
使用这些额外元数据值的能力非常有用,我也经常使用它们。长期以来,我支持 [Display] 和 [Description] 属性,但有一个请求是也要支持 [EnumMember]。问题是当你在枚举成员上有多个元数据属性时——生成器应该使用哪一个?以前,生成器会任意优先选择 [Display],然后回退到 [Description]。但那种排序没有充分的理由,完全是因为一个比另一个先实现而已😬。而将 [EnumMember] 作为另一个回退选项感觉太糟糕了。😅
因此,在 #163 中,我明确添加了对 [EnumMember] 的支持,但也更新了代码,使得对于给定的枚举,只能使用单一的元数据属性源。这意味着对于给定的枚举,只考虑一种类型的元数据属性。你可以通过设置 [EnumExtensions] 属性上的 MetadataSource 属性来选择要使用的源。在下面的例子中,生成的源代码明确选择使用 [Display] 属性:
|
|
应用于上述枚举成员中的任何其他元数据属性([Description]、[EnumMember])都将被忽略。或者,你可以使用 MetadataSource.None 来选择不使用任何元数据属性。在这种情况下,不会生成接受 useMetadataAttributes 参数的重载。
这本身就是一个破坏性变更,但还有一个更大的变化:默认的元数据源已更改为 [EnumMember],作为这些属性在语义上更好的选择。
你可以通过在项目中设置 EnumGenerator_EnumMetadataSource 属性来更改整个项目的默认元数据源:
|
|
重申一下,这是一个破坏性变更,如果你当前正在使用元数据属性,将会受到影响。我可能在后续版本中添加一个分析器来尝试警告这个潜在问题,这引出了下一个类别:分析器。
警告不正确使用的新分析器
在某些情况下,NetEscapades.EnumGenerators 包生成的代码将无法编译。这些通常是生成器中难以处理的边缘情况,但如果你在应用程序中遇到它们,可能会非常令人困惑。为了解决这个问题,我添加了几个 Roslyn 分析器来解释和警告那些会导致问题的情况。
标记生成的扩展类名称冲突
目前,你可以用 [EnumExtension] 属性装饰枚举,使得两种情况下都使用相同的扩展类名,从而导致名称冲突。例如,以下代码为每个枚举生成了 SomeNamespace.MyEnumExtensions 两次:
|
|
理想情况下,我们会通过为第二种情况生成嵌套类 SomeNamespace.Nested.MyEnumExtensions 来消除歧义,但不幸的是,扩展方法类不能是嵌套类。另一个选择是在生成的命名空间中包含类名,但这又会遇到另一个可能产生冲突的问题。最终,总是有一种方法会导致冲突,尤其是你可以显式设置要生成的类的名称!鉴于这些类型的冲突将非常罕见,#158 添加了一个分析器,诊断 ID 为 NEEG001,它直接在 [EnumExtensions] 属性上标记冲突,作为一个错误诊断。这并不是绝对必要的,因为生成重复的扩展类会导致大量编译器错误,但拥有一个分析器有望让问题更明显。😅
处理嵌套在泛型类型中的枚举
另一个我们根本无法生成有效代码的情况是,如果你有一个嵌套在泛型类型中的枚举:
|
|
不幸的是,在这种情况下没有简单的方法来生成有效的扩展类。我们不能将生成的扩展类放在 Nested<T> 内部,因为扩展方法不能放在嵌套类型中。我们可以通过使扩展类本身成为泛型类来做一些事情,但这都有点令人困惑,并为复杂性打开了闸门。相反,在 #159 中,我选择不支持这种场景。如果你编写了上述代码,则不会生成扩展方法,而是将 NEEG002 诊断应用于 [EnumExtensions] 属性,警告你这不是有效的。
枚举中的重复 case 标签
此版本中添加的最后一个分析器处理你有“重复”枚举成员的情况,即与其他成员具有相同“值”的枚举成员。例如,在下面的代码中,Failed 和 Error 具有相同的值:
|
|
这是完全有效的,但由于枚举生成器处理 switch 表达式的方式,这意味着如果你调用 ToStringFast()(或其他方法),你将不会总是得到预期的值。这本身不是生成器的问题,因为使用内置的 ToString() 方法也会看到类似的行为:
|
|
这只是 .NET 中枚举幕后工作方式的产物,但这可能会令人困惑,因此 #162 添加了一个分析器,用诊断 ID NEEG003 标记这些有问题的案例:
|
|
此诊断仅为信息级别,因此它不会中断你的构建,因为在这些情况下使用 [EnumExtensions] 仍然是有效的,只是重要的是要意识到生成的扩展可能不会按你预期的方式工作!
这涵盖了所有新的分析器,最后我们将看看一些修复。
错误修复
第一个修复在 #165 中引入,然后在 #172 中正确修复,是为了更好地处理用户将项目的 LangVersion 设置为 Preview 的情况。在 NetEscapades.EnumGenerators 的先前版本中,我添加了对 C#14 扩展成员的支持。这让你可以像静态扩展成员定义在类型本身上一样调用它们。例如,假设你有这个枚举:
|
|
源生成器生成一个 MyColoursExtensions.Parse() 方法,但有了扩展成员,你可以像它定义在 MyColours 枚举本身上一样调用它:
|
|
我原本打算只在您使用 C#14 时启用此功能,但我犯了一个错误。当您使用 C#14 或设置了 LangVersion=Preview 时,我都启用了它。长话短说,根据您面向的目标和构建时使用的 SDK 版本,Preview 几乎可以意味着任何东西,所以这不是一个好主意😅 作为修复,我移除了扩展成员的生成,除非您明确面向 C#14 或更高版本(忽略 Preview 情况)。为了在使用 Preview 时允许选择加入扩展成员,我添加了一个 EnumGenerator_ForceExtensionMembers 设置,您可以将其设置为 true 以在通常不会生成的情况下明确选择加入。不幸的是,我最初不小心将其默认设置为 true,所以 #172 将其修复为默认 false。🙈
另一个主要的修复是为了处理枚举成员名称为保留字的情况,例如:
|
|
不幸的是,我没有正确处理这个问题,所以生成器生成了无效的代码:
|
|
修复涉及用这个方便的函数更新生成器,以确保我们根据需要正确转义标识符:
|
|
这样生成的代码就能正确转义:
|
|
最后一个变化是在 #160 中移除了 NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES 选项,这移除了将标记属性嵌入目标 dll 的能力。这很少是正确的事情,而且该包已经在做将属性放在专用 dll 中的工作。这也减少了一些重复,删除了需要测试的配置组合,并为未来在“属性”dll 中交付“辅助”类型打开了可能性。
总结
在这篇文章中,我介绍了 NetEscapades.EnumGenerators 在 1.0.0-beta16 版本中发布的一些近期更新。这些生活质量更新添加了对 [EnumMember] 的支持,更新了元数据属性的使用方式,并添加了额外的分析器来捕获潜在的陷阱。最后,它修复了一些边缘情况的错误。如果你还没有更新,我建议你更新并尝试一下!如果你遇到任何问题,请在 GitHub 上记录一个问题。🙂