You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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.CSharpMicrosoft.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

火山引擎 最新活动