C#自定义特性能否作为功能工具实现方法自动注册?
完全可行!用C#自定义特性+源代码生成器就能实现你的需求
你的思路非常棒——自定义特性+编译时代码生成正好能解决「自动注册方法到列表、避免运行时性能损耗、松耦合」这些核心需求,很多大型框架(比如ASP.NET Core的路由系统、MediatR的请求处理注册)都是类似的思路,只不过现在我们可以用Roslyn源代码生成器把注册逻辑提前到编译阶段,彻底摆脱运行时反射的开销。
下面我给你拆解下具体的实现步骤:
1. 第一步:定义自定义特性
首先我们需要创建一个标记用的自定义特性,用来标注需要自动注册的方法:
// 特性类,用来标记需要注册的命令方法 [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] public sealed class CommandAttribute : Attribute { // 可以按需添加属性,比如给命令指定唯一索引/名称 public string? CommandName { get; set; } // 如果需要数字索引,也可以加int类型的Index属性 public int? CommandIndex { get; set; } }
2. 第二步:用Roslyn源代码生成器实现编译时自动注册
这是核心环节——我们要写一个源代码生成器,在编译时扫描所有带有[Command]特性的方法,自动生成静态的方法注册代码。这样运行时不需要反射,直接调用生成的代码即可。
2.1 创建源代码生成器项目
你需要创建一个.NET类库项目,引用Microsoft.CodeAnalysis.CSharp和Microsoft.CodeAnalysis.Analyzers NuGet包,然后实现ISourceGenerator接口:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; [Generator] public class CommandGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { // 注册语法接收器,用来收集带有[Command]特性的方法 context.RegisterForSyntaxNotifications(() => new CommandSyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is not CommandSyntaxReceiver receiver) return; // 准备生成代码的字符串 var sourceBuilder = new StringBuilder(@" using System; using System.Collections.Generic; namespace YourFrameworkNamespace { public static class CommandRegistry { // 存储所有注册的命令方法,用委托封装 public static readonly Dictionary<object, Delegate> Commands = new Dictionary<object, Delegate>(); static CommandRegistry() { "); // 遍历收集到的所有带[Command]的方法 foreach (var method in receiver.CommandMethods) { var methodSymbol = context.SemanticModel.GetDeclaredSymbol(method) as IMethodSymbol; if (methodSymbol == null || !methodSymbol.IsPublic) continue; // 只处理公共方法 // 获取特性的配置:优先用CommandIndex,没有则用CommandName,最后用方法名 var commandAttr = methodSymbol.GetAttributes() .First(attr => attr.AttributeClass?.Name == nameof(CommandAttribute)); var indexArg = commandAttr.NamedArguments.FirstOrDefault(arg => arg.Key == nameof(CommandAttribute.CommandIndex)); var nameArg = commandAttr.NamedArguments.FirstOrDefault(arg => arg.Key == nameof(CommandAttribute.CommandName)); var key = indexArg.Value.Value ?? (nameArg.Value.Value as string) ?? methodSymbol.Name; // 根据方法参数生成对应的委托类型 var delegateType = GetDelegateType(methodSymbol); if (delegateType == null) continue; // 生成注册代码 sourceBuilder.AppendLine($@" Commands.Add({GetKeyLiteral(key)}, new {delegateType}({methodSymbol.ContainingType.FullName}.{methodSymbol.Name}));"); } // 完成代码生成 sourceBuilder.Append(@" } private static string GetKeyLiteral(object key) => key switch { int i => i.ToString(), string s => $@""{s}"", _ => throw new NotSupportedException(""不支持的命令键类型"") }; } }"); // 把生成的代码添加到编译过程中 context.AddSource("CommandRegistry.g.cs", sourceBuilder.ToString()); } // 根据方法参数返回对应的委托类型名称 private string? GetDelegateType(IMethodSymbol method) { var paramCount = method.Parameters.Length; var paramTypes = string.Join(", ", method.Parameters.Select(p => p.Type.ToString())); var returnType = method.ReturnType.ToString(); if (returnType == "System.Void") { return paramCount switch { 0 => "Action", 1 => $"Action<{paramTypes}>", 2 => $"Action<{paramTypes}>", 3 => $"Action<{paramTypes}>", _ => null // 可按需扩展更多参数 }; } else { return paramCount switch { 0 => $"Func<{returnType}>", 1 => $"Func<{paramTypes}, {returnType}>", _ => null // 可按需扩展更多参数 }; } } // 语法接收器:用来在编译时收集带有[Command]特性的方法 private class CommandSyntaxReceiver : ISyntaxReceiver { public List<MethodDeclarationSyntax> CommandMethods { get; } = new List<MethodDeclarationSyntax>(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { // 只关心公共方法声明,并且带有[Command]特性 if (syntaxNode is MethodDeclarationSyntax methodDeclaration && methodDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)) && methodDeclaration.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == nameof(CommandAttribute)))) { CommandMethods.Add(methodDeclaration); } } } }
3. 第三步:在业务项目(或DLL)中使用特性标记方法
不管是你的主项目还是独立DLL类库,只要引用了特性项目和生成器项目,就可以直接用[Command]标记方法:
// 主项目或者DLL中的类 public class PlayerCommands { [Command(CommandIndex = 0)] // 用数字索引 public void DisconnectPlayer(Player player) { // 你的业务逻辑 Console.WriteLine($"玩家{player.Id}已断开连接"); } [Command(CommandName = "Kick")] // 用自定义名称当索引 public void KickPlayer(Player player, string reason) { Console.WriteLine($"玩家{player.Id}因{reason}被踢出"); } [Command] // 默认用方法名当索引 public void BroadcastMessage(string message) { Console.WriteLine($"广播消息:{message}"); } }
4. 第四步:通过索引调用注册的方法
运行时直接使用生成的CommandRegistry类,通过索引(数字/名称)调用对应的方法:
// 通过数字索引调用DisconnectPlayer var disconnectCmd = CommandRegistry.Commands[0] as Action<Player>; disconnectCmd?.Invoke(new Player { Id = 123 }); // 通过名称索引调用KickPlayer var kickCmd = CommandRegistry.Commands["Kick"] as Action<Player, string>; kickCmd?.Invoke(new Player { Id = 456 }, "违规操作"); // 通过方法名调用BroadcastMessage var broadcastCmd = CommandRegistry.Commands["BroadcastMessage"] as Action<string>; broadcastCmd?.Invoke("欢迎加入游戏!");
关于松耦合和DLL支持的说明
- 松耦合:框架只依赖
CommandAttribute和自动生成的CommandRegistry,业务类不需要继承任何基类或实现特定接口,完全是基于特性的标记,彻底实现框架与业务代码的解耦。 - DLL支持:只要DLL项目引用了特性和生成器,编译时会自动在DLL中生成对应的注册逻辑,主项目引用DLL后,
CommandRegistry会自动包含DLL中所有标记的方法(源代码生成器会在每个项目编译时处理该项目内的标记方法,最终所有项目的注册代码会合并到统一的CommandRegistry中)。
额外优化建议
- 可以在生成器中添加编译错误提示,比如检测到私有方法、参数过多等不符合要求的情况时,直接在编译阶段抛出错误,提前发现问题。
- 如果需要更灵活的方法封装,可以考虑用
Delegate.DynamicInvoke,但不推荐(会有性能损耗),最好还是提前生成强类型委托。 - 可以给
CommandRegistry添加泛型方法,简化调用时的类型转换,比如public static T GetCommand<T>(object key) where T : Delegate。
内容的提问来源于stack exchange,提问作者user2696482




