我正在使用C#开发命令工具,尽管不是用于终端命令行。我已经阅读了有关反射和属性的文档,但是我不确定确切的解决方法是什么。
问题不是很复杂,但是需要轻松扩展。我只需要在检查其触发字符串的位置拾取并加载命令,如果它们匹配,就调用方法。正如概念验证一样,我是如何做到的:
[System.AttributeUsage(System.AttributeTargets.Class)]
public class CommandAttribute : Attribute
{
public string Name { get; private set; } //e.g Help
public string TriggerString { get; private set; } //e.g. help, but generally think ls, pwd, etc
public CommandAttribute(string name, string triggerStrings)
{
this.Name = name;
this.TriggerString = triggerString;
}
}
现在,我装饰了该类,它将通过接口实现方法。最终将有许多命令,而我的想法是使编程经验最少的人可以轻松进入并创建命令。
using Foo.Commands.Attributes;
using Foo.Infrastructure;
namespace Foo.Commands
{
[Command("Help", "help")]
public class Help : IBotCommand
{
// as an example, if the message's contents match up with this command's triggerstring
public async Task ExecuteAction()
}
}
这被注入到控制台应用程序中,它将在其中加载命令并获取传递的消息
public interface ICommandHandler
{
Task LoadCommands();
Task CheckMessageForCommands();
}
然后,将加载具有匹配属性的所有内容,并且在接收到消息时,它将根据所有CommandAttribute装饰类的触发字符串检查其内容,如果匹配,请对该命令类调用ExecuteAction方法。
我所见/尝试过的:我了解如何使用反射来获取自定义属性数据,但是我对于获取方法和调用它们以及如何将所有这些配置为在反射时具有相当的性能感到困惑正在使用。我看到了使用类似方法的CLI工具和聊天机器人,我只是无法窥视它们的处理程序以了解如何加载这些处理程序,也找不到任何资源来解释如何访问这些处理程序的methods类。在这里,属性可能不是正确的答案,但我不确定该怎么做。
真的,我的主要问题是:
我的解决方案最终仅使用激活器和列表。我仍然需要对此进行调整以提高性能并运行更广泛的压力测试,但这是我的快速代码:
// for reference: DiscordCommandAttribute is in Foo.Commands library where all the commands are, so for now it's the target as I removed the base class
// IDiscordCommand has every method needed, so casting it as that means down the line I can call my methods off of it. The base class was just for some reflection logic I was testing and was removed, so it's gone
public void LoadCommands() // called in ctor
{
var commands =
from t in typeof(DiscordCommandAttribute).Assembly.GetTypes()
let attribute = t.GetCustomAttribute(typeof(DiscordCommandAttribute), true)
where attribute != null
select new { Type = t, Attribute = attribute };
foreach (var obj in commands)
{
_commandInstances.Add((IDiscordCommand)Activator.CreateInstance(obj.Type));
_commandAttributes.Add(obj.Attribute as DiscordCommandAttribute);
}
}
可能有一种更灵活的方式来处理将对象添加到列表中的操作,而列表以外的其他数据结构可能更合适,我只是不确定HashSet是否正确,因为这不是直接的Equals调用。最终,我将泛化此类的接口并将所有这些逻辑隐藏在基类中。还有很多工作要做。
[目前,仅在调用LoadCommands之前放秒表会显示整个加载需要4毫秒。这具有3个类和一个非常贫乏的属性,但是我不太担心比例,因为我希望在启动时而不是在命令处理过程中产生任何开销。
使用我为this answer编写的一些代码,您可以找到实现interface
的所有类型,例如IBotCommand
,然后检索自定义属性:
public static class TypeExt {
public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));
static Dictionary<Type, HashSet<Type>> FoundTypes = null;
static List<Type> LoadableTypes = null;
public static void RefreshLoadableTypes() {
LoadableTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetLoadableTypes()).ToList();
FoundTypes = new Dictionary<Type, HashSet<Type>>();
}
public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) {
if (FoundTypes != null && FoundTypes.TryGetValue(interfaceType, out var ft))
return ft;
else {
if (LoadableTypes == null)
RefreshLoadableTypes();
var ans = LoadableTypes
.Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
(includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
(includeStructs || !aType.IsValueType) &&
(includeSystemTypes || !aType.IsBuiltin()) &&
interfaceType.IsAssignableFrom(aType) &&
aType.GetInterfaces().Contains(interfaceType))
.ToHashSet();
FoundTypes[interfaceType] = ans;
return ans;
}
}
}
public static class AssemblyExt {
//https://stackoverflow.com/a/29379834/2557128
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
if (assembly == null)
throw new ArgumentNullException("assembly");
try {
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e) {
return e.Types.Where(t => t != null);
}
}
}
注意:如果在运行时创建类型,则需要运行RefreshLoadableTypes
以确保返回它们。
如果您担心不存在IBotCommand
的CommandAttribute
实现者,则可以过滤ImplementingTypes
,否则:
var botCommands = typeof(IBotCommand)
.ImplementingTypes()
.Select(t => new { Type = t, attrib = t.GetTypeInfo().GetCustomAttribute<CommandAttribute>(false) })
.Select(ta => new {
ta.Type,
TriggerString = ta.attrib.TriggerString
})
.ToDictionary(tct => tct.TriggerString, tct => tct.Type);
使用命令Type
的扩展方法:
public static class CmdTypeExt {
public static Task ExecuteAction(this Type commandType) {
var cmdObj = (IBotCommand)Activator.CreateInstance(commandType);
return cmdObj.ExecuteAction();
}
}
您可以像使用Dictionary
:
var cmdString = Console.ReadLine();
if (botCommands.TryGetValue(cmdString, out var cmdType))
await cmdType.ExecuteAction();
总体上,我可能建议在静态类中为命令提供方法属性并具有静态方法,因此可以将多个(相关的?)命令捆绑在一个类中。
PS My命令解释器具有与每个命令关联的帮助,以及将命令分组的类别,因此,也许还有更多属性参数和/或另一个IBotCommand
方法可返回帮助字符串。