前言
C# 获取枚举描述的方法有很多, 常用的有通过
DescriptionAttribute
反射获取, 进阶的可以加上缓存机制, 减少反射的开销。今天我们还提供一种更加高效的方法,通过增量源生成器生成获取枚举描述的代码。这是在编译层面实现的, 无需反射, 性能更高。
本文的演示代码基于 VS2022 + .NET 8.0 + .NET Standard 2.0
1. 基本反射
这种方法是最常用的方法, 但是反射开销比较大。
public enum Color { [Description("红色")] Red, [Description("绿色")] Green, [Description("蓝色")] Blue } public static string GetDescription(Color color) { var fieldInfo = typeof(Color).GetField(color.ToString()); var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>(); return descriptionAttribute?.Description; }
2. 反射 + 缓存
缓存机制可以减少反射的开销, 避免反射过于频繁。
private static readonly Dictionary<Color, string> _descriptionCache = new Dictionary<Color, string>(); public static string GetDescription(Color color) { if (_descriptionCache.TryGetValue(color, out var description)) { return description; } var fieldInfo = typeof(Color).GetField(color.ToString()); var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>(); description = descriptionAttribute?.Description; _descriptionCache.Add(color, description); return description; }
3. 反射 + 缓存 + 泛型类 (推荐)
泛型可以减少代码重复。下面的代码为基本实现, 没有考虑线程安全问题。线程安全问题可以通过锁机制解决。可以使用静态构造函数初始化缓存。或者使用 ConcurrentDictionary 代替 Dictionary。或者使用 Lazy
代替缓存。
public class EnumDescription<T> where T : Enum { private static readonly Dictionary<T, string> _descriptionCache = new Dictionary<T, string>(); public static string GetDescription(T value) { if (_descriptionCache.TryGetValue(value, out var description)) { return description; } var fieldInfo = typeof(T).GetField(value.ToString()); var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>(); description = descriptionAttribute?.Description; _descriptionCache.Add(value, description); return description; } }
4. 增量源生成器 (消除反射)
创建增量源生成器类库项目 (.NET Standard 2.0)
-
创建一个基于 .NET Standard 2.0 的类库项目名为:
SourceGenerator
-
添加 NuGet 包
Microsoft.CodeAnalysis.CSharp
版本4.8.0
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" /> </ItemGroup> </Project>
- 添加
EnumDescriptionGenerator
类, 实现IIncrementalGenerator
接口
using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; [Generator] public class EnumDescriptionGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var enumDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: (syntaxNode, _) => syntaxNode is EnumDeclarationSyntax, transform: (generatorSyntaxContext, _) => { var enumDeclaration = (EnumDeclarationSyntax)generatorSyntaxContext.Node; var enumSymbol = generatorSyntaxContext.SemanticModel.GetDeclaredSymbol(enumDeclaration) as INamedTypeSymbol; return new { EnumDeclaration = enumDeclaration, EnumSymbol = enumSymbol }; }) .Where(t => t.EnumSymbol != null) .Collect(); var compilationAndEnums = context.CompilationProvider.Combine(enumDeclarations); context.RegisterSourceOutput(compilationAndEnums, (sourceProductionContext, tuple) => { var compilation = tuple.Left; var enums = tuple.Right; foreach (var item in enums) { var enumDeclaration = item.EnumDeclaration; var enumSymbol = item.EnumSymbol; if (!enumSymbol.GetMembers("GetDescription").Any()) { var source = GenerateSourceCode(enumSymbol); sourceProductionContext.AddSource($"{enumSymbol.Name}Descriptions.g.cs", SourceText.From(source, Encoding.UTF8)); } } }); } // 生成枚举描述扩展方法的代码 private static string GenerateSourceCode(INamedTypeSymbol enumSymbol) { var enumName = enumSymbol.Name; var namespaceName = enumSymbol.ContainingNamespace?.ToString() ?? "Global"; var sb = new StringBuilder(); sb.AppendLine($"namespace {namespaceName};"); sb.AppendLine($"public static partial class {enumName}Extensions"); sb.AppendLine("{"); sb.AppendLine($" public static string GetDescription(this {enumName} value) =>"); sb.AppendLine(" value switch"); sb.AppendLine(" {"); // 4. 遍历枚举成员 foreach (var member in enumSymbol.GetMembers().Where(m => m.Kind == SymbolKind.Field)) { var description = member.GetAttributes() .FirstOrDefault(a => a.AttributeClass?.Name == "DescriptionAttribute") ?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? member.Name; sb.AppendLine($" {enumName}.{member.Name} => "{description}","); } sb.AppendLine(" _ => string.Empty"); sb.AppendLine(" };"); sb.AppendLine("}"); return sb.ToString(); } }
创建控制台主项目 MainProject
- 使用 .NET 8.0 , 引用
SourceGenerator
项目, 注意引用方式如下:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <ProjectReference Include="..SourceGeneratorSourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup> </Project>
- 在
MainProject
中使用生成的枚举描述扩展方法
namespace MainProject; class Program { static void Main() { foreach (var color in Enum.GetValues<Color>()) { Console.WriteLine(color.GetDescription()); } Console.ReadKey(); } }
- 编译运行, 编译器会自动生成枚举描述扩展方法的代码。
演示程序截图:
总结
通过增量源生成器, 我们可以在编译期自动生成获取枚举描述的代码, 无需反射, 性能更高。