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)
-
創建一個基于 .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="..\SourceGenerator\SourceGenerator.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();
}
}
- 編譯運行, 編譯器會自動生成枚舉描述擴展方法的代碼。
演示程序截圖:


總結
通過增量源生成器, 我們可以在編譯期自動生成獲取枚舉描述的代碼, 無需反射, 性能更高。

浙公網安備 33010602011771號