C# – 获取枚举描述 – 使用增量源生成器

前言

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)

  1. 创建一个基于 .NET Standard 2.0 的类库项目名为: SourceGenerator

  2. 添加 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> 
  1. 添加 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

  1. 使用 .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>  
  1. MainProject 中使用生成的枚举描述扩展方法
namespace MainProject;  class Program {     static void Main()     {         foreach (var color in Enum.GetValues<Color>())         {             Console.WriteLine(color.GetDescription());         }         Console.ReadKey();     } } 
  1. 编译运行, 编译器会自动生成枚举描述扩展方法的代码。

演示程序截图:

C# - 获取枚举描述 - 使用增量源生成器

C# - 获取枚举描述 - 使用增量源生成器

总结

通过增量源生成器, 我们可以在编译期自动生成获取枚举描述的代码, 无需反射, 性能更高。

发表评论

您必须 [ 登录 ] 才能发表留言!

相关文章