C# - 获取罗列形貌 - 使用增量源生成器

打印 上一主题 下一主题

主题 948|帖子 948|积分 2844

前言

C# 获取罗列形貌的方法有很多, 常用的有通过 DescriptionAttribute 反射获取, 进阶的可以加上缓存机制, 减少反射的开销。本日我们还提供一种更加高效的方法,通过增量源生成器生成获取罗列形貌的代码。这是在编译层面实现的, 无需反射, 性能更高。
本文的演示代码基于 VS2022 + .NET 8.0 + .NET Standard 2.0
1. 基本反射

这种方法是最常用的方法, 但是反射开销比较大。
  1. public enum Color
  2. {
  3.     [Description("红色")]
  4.     Red,
  5.     [Description("绿色")]
  6.     Green,
  7.     [Description("蓝色")]
  8.     Blue
  9. }
  10. public static string GetDescription(Color color)
  11. {
  12.     var fieldInfo = typeof(Color).GetField(color.ToString());
  13.     var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>();
  14.     return descriptionAttribute?.Description;
  15. }
复制代码
2. 反射 + 缓存

缓存机制可以减少反射的开销, 避免反射过于频繁。
  1. private static readonly Dictionary<Color, string> _descriptionCache = new Dictionary<Color, string>();
  2. public static string GetDescription(Color color)
  3. {
  4.     if (_descriptionCache.TryGetValue(color, out var description))
  5.     {
  6.         return description;
  7.     }
  8.     var fieldInfo = typeof(Color).GetField(color.ToString());
  9.     var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>();
  10.     description = descriptionAttribute?.Description;
  11.     _descriptionCache.Add(color, description);
  12.     return description;
  13. }
复制代码
3. 反射 + 缓存 + 泛型类 (保举)

泛型可以减少代码重复。下面的代码为基本实现, 没有考虑线程安全问题。线程安全问题可以通过锁机制解决。可以使用静态构造函数初始化缓存。或者使用 ConcurrentDictionary 代替 Dictionary。或者使用 Lazy 代替缓存。
  1. public class EnumDescription<T> where T : Enum
  2. {
  3.     private static readonly Dictionary<T, string> _descriptionCache = new Dictionary<T, string>();
  4.     public static string GetDescription(T value)
  5.     {
  6.         if (_descriptionCache.TryGetValue(value, out var description))
  7.         {
  8.             return description;
  9.         }
  10.         var fieldInfo = typeof(T).GetField(value.ToString());
  11.         var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>();
  12.         description = descriptionAttribute?.Description;
  13.         _descriptionCache.Add(value, description);
  14.         return description;
  15.     }
  16. }
复制代码
4. 增量源生成器 (消除反射)

创建增量源生成器类库项目 (.NET Standard 2.0)


  • 创建一个基于 .NET Standard 2.0 的类库项目名为: SourceGenerator
  • 添加 NuGet 包 Microsoft.CodeAnalysis.CSharp 版本 4.8.0
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <TargetFramework>netstandard2.0</TargetFramework>
  4.                 <LangVersion>latest</LangVersion>
  5.                 <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  6.         </PropertyGroup>
  7.         <ItemGroup>
  8.                 <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
  9.         </ItemGroup>
  10. </Project>
复制代码

  • 添加 EnumDescriptionGenerator 类, 实现 IIncrementalGenerator 接口
  1. using System.Linq;
  2. using System.Text;
  3. using Microsoft.CodeAnalysis;
  4. using Microsoft.CodeAnalysis.CSharp.Syntax;
  5. using Microsoft.CodeAnalysis.Text;
  6. [Generator]
  7. public class EnumDescriptionGenerator : IIncrementalGenerator
  8. {
  9.     public void Initialize(IncrementalGeneratorInitializationContext context)
  10.     {
  11.         var enumDeclarations = context.SyntaxProvider
  12.            .CreateSyntaxProvider(
  13.                 predicate: (syntaxNode, _) => syntaxNode is EnumDeclarationSyntax,
  14.                 transform: (generatorSyntaxContext, _) =>
  15.                 {
  16.                     var enumDeclaration = (EnumDeclarationSyntax)generatorSyntaxContext.Node;
  17.                     var enumSymbol = generatorSyntaxContext.SemanticModel.GetDeclaredSymbol(enumDeclaration) as INamedTypeSymbol;
  18.                     return new { EnumDeclaration = enumDeclaration, EnumSymbol = enumSymbol };
  19.                 })
  20.            .Where(t => t.EnumSymbol != null)
  21.            .Collect();
  22.         var compilationAndEnums = context.CompilationProvider.Combine(enumDeclarations);
  23.         context.RegisterSourceOutput(compilationAndEnums, (sourceProductionContext, tuple) =>
  24.         {
  25.             var compilation = tuple.Left;
  26.             var enums = tuple.Right;
  27.             foreach (var item in enums)
  28.             {
  29.                 var enumDeclaration = item.EnumDeclaration;
  30.                 var enumSymbol = item.EnumSymbol;
  31.                 if (!enumSymbol.GetMembers("GetDescription").Any())
  32.                 {
  33.                     var source = GenerateSourceCode(enumSymbol);
  34.                     sourceProductionContext.AddSource($"{enumSymbol.Name}Descriptions.g.cs", SourceText.From(source, Encoding.UTF8));
  35.                 }
  36.             }
  37.         });
  38.     }
  39.     // 生成枚举描述扩展方法的代码
  40.     private static string GenerateSourceCode(INamedTypeSymbol enumSymbol)
  41.     {
  42.         var enumName = enumSymbol.Name;
  43.         var namespaceName = enumSymbol.ContainingNamespace?.ToString() ?? "Global";
  44.         var sb = new StringBuilder();
  45.         sb.AppendLine($"namespace {namespaceName};");
  46.         sb.AppendLine($"public static partial class {enumName}Extensions");
  47.         sb.AppendLine("{");
  48.         sb.AppendLine($"    public static string GetDescription(this {enumName} value) =>");
  49.         sb.AppendLine("        value switch");
  50.         sb.AppendLine("        {");
  51.         // 4. 遍历枚举成员
  52.         foreach (var member in enumSymbol.GetMembers().Where(m => m.Kind == SymbolKind.Field))
  53.         {
  54.             var description = member.GetAttributes()
  55.                 .FirstOrDefault(a => a.AttributeClass?.Name == "DescriptionAttribute")
  56.                 ?.ConstructorArguments.FirstOrDefault().Value?.ToString()
  57.                 ?? member.Name;
  58.             sb.AppendLine($"            {enumName}.{member.Name} => "{description}",");
  59.         }
  60.         sb.AppendLine("            _ => string.Empty");
  61.         sb.AppendLine("        };");
  62.         sb.AppendLine("}");
  63.         return sb.ToString();
  64.     }
  65. }
复制代码
创建控制台主项目 MainProject


  • 使用 .NET 8.0 , 引用 SourceGenerator 项目, 留意引用方式如下:
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <OutputType>Exe</OutputType>
  4.                 <TargetFramework>net8.0</TargetFramework>
  5.                 <ImplicitUsings>enable</ImplicitUsings>
  6.                 <Nullable>enable</Nullable>
  7.         </PropertyGroup>
  8.         <ItemGroup>
  9.                 <ProjectReference Include="..\SourceGenerator\SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  10.         </ItemGroup>
  11. </Project>
复制代码

  • 在 MainProject 中使用生成的罗列形貌扩展方法
  1. namespace MainProject;
  2. class Program
  3. {
  4.     static void Main()
  5.     {
  6.         foreach (var color in Enum.GetValues<Color>())
  7.         {
  8.             Console.WriteLine(color.GetDescription());
  9.         }
  10.         Console.ReadKey();
  11.     }
  12. }
复制代码

  • 编译运行, 编译器会自动生成罗列形貌扩展方法的代码。
演示程序截图:



总结

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

何小豆儿在此

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表