Recent updates to NetEscapades.EnumGenerators: [EnumMember] support, analyzers, and bug fixes
在这篇文章中,我将介绍我创建的源码生成器NuGet包NetEscapades.EnumGenerators最近的一些更新。该包可用于为枚举添加快速操作方法。我将首先介绍这个包存在的原因及其用途,然后介绍一些最近的改动。
为什么应该使用枚举源码生成器?
NetEscapades.EnumGenerators是我使用.NET 6中引入的增量生成器支持创建的首批源码生成器之一。我选择创建这个包是为了解决使用枚举时一个恼人的特性:某些操作速度出奇地慢。
注意:虽然这在历史上是事实,但这一情况未必会永远持续。事实上,.NET 8+在运行时中对枚举处理进行了大量改进。
例如,假设你有以下枚举:
|
|
在某个时刻,你想打印一个Color变量的名称,所以你创建了这个辅助方法:
|
|
虽然这看起来应该很快,但实际上并非如此。NetEscapades.EnumGenerators通过自动生成一个快速的实现来解决这个问题。它会生成一个ToStringFast()方法,看起来像这样:
|
|
这个简单的switch语句检查Colour的每个已知值,并使用nameof返回枚举的文本表示。如果是一个未知值,则回退到内置的ToString()实现,以确保正确处理未知值(例如,这段C#代码是有效的:PrintColour((Colour)123))。如果我们使用BenchmarkDotNet比较这两种实现对于一个已知颜色的性能,你可以看到ToStringFast()实现要快得多:
| Method | FX | Mean | Error | StdDev | Ratio | Gen 0 | Allocated |
|---|---|---|---|---|---|---|---|
| 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.005 | - | - |
| 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.007 | - | - |
这些数字现在显然已经很旧了,但总体模式没有改变:.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]属性,警告你此用法无效。
枚举中的重复标签
此版本中添加的最后一个分析器处理“重复”枚举成员的情况,即枚举成员与其他成员具有相同的“值”。例如,在下面的代码中,Failed和Error具有相同的值:
|
|
这完全是有效的,但由于枚举生成器使用switch表达式的方式,这意味着如果你调用ToStringFast()(或其他方法),不一定总能得到你期望的值。这本身不是生成器的问题,因为使用内置的ToString()方法时也会看到类似的行为:
|
|
这只是.NET中枚举幕后工作方式的一个产物,但这可能会令人困惑,所以#162添加了一个分析器,用诊断NEEG003标记这些有问题的情况:
|
|
这个诊断只是信息级别,所以它不会中断你的构建,因为在这些情况下使用[EnumExtensions]仍然是有效的,只是需要注意生成的扩展可能不会如你预期的那样工作!
这涵盖了所有新的分析器,最后我们来看一些修复。
错误修复
第一个修复在#165中引入,然后在#172中正确修复,目的是更好地处理用户将其项目的LangVersion设置为Preview的情况。在NetEscapades.EnumGenerators的先前版本中,我添加了对C#14扩展成员的支持。这让你可以像在类型本身定义一样调用静态扩展成员。例如,假设你有这个枚举:
|
|
源码生成器生成一个MyColoursExtensions.Parse()方法,但有了扩展成员,你可以像在MyColours枚举本身上定义一样调用它:
|
|
我原本打算只在您使用C#14时启用此功能,但我犯了一个错误。当您使用C#14或设置了LangVersion=Preview时,我都启用了它。长话短说,Preview可能意味着几乎任何事情,具体取决于您针对的目标和构建时使用的SDK版本,所以这不是一个好主意。😅
作为修复,我移除了扩展成员的生成,除非您明确针对C#14或更高版本(忽略Preview情况)。为了允许在使用Preview时选择加入扩展成员,我添加了一个EnumGenerator_ForceExtensionMembers设置,您可以将其设置为true以在通常不会生成时显式选择加入。不幸的是,我最初意外地将其默认设置为true,所以#172将其修复为默认false。🙈
另一个主要修复是处理枚举成员名是保留字的情况,例如:
|
|
不幸的是,我之前没有正确处理这个问题,所以生成器生成了无效的代码:
|
|
修复涉及使用这个方便的函数更新生成器,以确保在必要时正确转义标识符:
|
|
从而使生成的代码被正确转义:
|
|
最后一项更改是在#160中移除了NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES选项,该选项移除了将标记属性嵌入目标dll的能力。这很少是正确的做法,而且包已经在专门用于在一个专用的dll中分发该属性。这也减少了一些重复,移除了一个需要测试的配置组合,并为将来在“attributes” dll中分发“helper”类型开辟了可能性。
总结
在本文中,我介绍了NetEscapades.EnumGenerators在1.0.0-beta16版本中发布的一些最新更新。这些生活质量更新添加了对[EnumMember]的支持,更新了元数据属性的使用方式,并添加了额外的分析器以捕获潜在的陷阱。最后,它修复了几个边缘情况的错误。如果你还没有更新,我建议你更新并尝试一下!如果遇到任何问题,请在GitHub上提交问题。🙂