为字符串调用适当的方法

本文关键字:方法 字符串 调用 | 更新日期: 2023-09-27 17:56:05

这本身就有一个游戏开发项目,但它实际上是关于编码数据并将其映射到其他数据片段。这就是为什么我决定在这里发布它。

我用于外部库存物料数据存储的格式:

[ID:IT_FO_TROUT]
[Name:Trout]
[Description:A raw trout.]
[Value:10]
[3DModel:null]
[InventoryIcon:trout]
[Tag:Consumable]
[Tag:Food]
[Tag:Stackable]
[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]

问题集中在最后 2 个 OnConsumption 属性上。基本上,这两个属性意味着当物品被消耗时,消费者的生命值会上升 15 点,他的疲劳也会增加。这在后台调用了 2 种不同的方法:

void RestoreHealth(Character Subject, int Amount);
void RestoreFatigue(Character Subject, int Amount);

您将如何将方法映射到它们的文件内字符串对应项?这是我的想法:

  1. 每次使用项目时,都会将字符串列表(事件)传递给项目事件管理器。管理器分析每个字符串并调用相应的方法。非常容易设置,并且由于这不是一个经常发生的操作,因此对性能的影响可能不大(字符串的大小也很小(最多 10-15 个字符),并在 O(n) 时间内解析)。

  2. 每个清单项(类)在初始化时解析字符串事件一次,并且只分析一次。每个字符串事件都通过字典映射到其相应的方法。就性能而言,这是我能想到的最有效的方法,但它使执行其他操作变得非常困难:字典中的所有值都必须是相同类型的委托。这意味着我无法保留

    a) 恢复健康(int)

    b) 召唤怪物(位置,计数)

    在同一字典中,并且必须为每种可调用方法设置新的数据结构。这是一项巨大的工作要做。

想到了一些方法来改进这两种方法:

  1. 我可以在 Item 事件中使用某种临时缓存管理器,以便不会解析项目的 OnConsumption 事件两次?我可能会遇到与我在 2 期间遇到的问题相同的问题)但是,由于缓存必须是map<InventoryItem,List<delegate>>.

  2. .NET 库中的哈希表数据结构允许 使任何类型的对象在任何给定时间成为键和/或值 (与字典不同)。我可以使用它并将字符串 A 映射到 委托 X,同时还具有映射到委托 Y 的字符串 B 在相同的结构内。我不应该这样做的任何理由?能 您预见到这种方法会带来任何麻烦吗?

我也在思考一些反思的方式,但当涉及到它时,我并不完全有经验。而且我很确定每次解析字符串会更快。

编辑

我的最终解决方案,考虑到阿列克谢·拉加的答案。对每种事件使用接口。

public interface IConsumeEvent    
{    
    void ApplyConsumeEffects(BaseCharacter Consumer);   
}

示例实现者(特定事件):

public class RestoreHealthEvent : IConsumeEvent
{    
    private int Amount = Amount;
    public RestoreHealthEvent(int Amount)
    {
        this.Amount = Amount;
    }
    public void ApplyConsumeEffects(BaseCharacter Consumer)   
    {
        Consumer.Stats.AlterStat(CharacterStats.CharStat.Health, Amount);    
    }    
}

在解析器内部(我们唯一关心事件特殊性的地方 - 因为我们正在解析数据文件本身):

RestoreHealthEvent ResHealthEv = new RestoreHealthEvent (Value);
NewItem.ConsumeEvents.Add (ResHealthEv );

当角色消耗物品时:

foreach (IConsumeEvent ConsumeEvent in Item.ConsumeEvents)
{
    //We're inside a parent method that's inside a parent BaseCharacter class; we're consuming an item right now.
    ConsumeEvent.ApplyConsumeEffects(this);
}

为字符串调用适当的方法

为什么不将它们"映射"到"命令"类一次,而且只有一次呢?

例如

[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]

可以映射到RestoreHealthRestoreFatigue命令类,这些命令类可以定义为:

public sealed class RestoreHealth : ICommand {
    public int Value { get; set; }
    //whatever else you need
}
public sealed class SummonMonster : ICommand {
    public int Count {get; set; }
    public Position Position { get; set; }
}

此时,将命令视为参数的包装器;)因此,您始终包装它们并仅传递一个参数,而不是传递多个参数。它还提供了一些语义。

现在,您可以将库存物品映射到在使用每个物品时需要"发送"的命令

您可以实现一个简单的"总线"接口,例如:

public interface IBus {
    void Send(ICommand command);
    void Subscribe(object subscriber);
}

现在,您只需获得IBus的实例,并在适当的时候调用其Send方法。

通过这样做,您将您的"定义"(需要做什么)和逻辑(如何执行操作)问题分开。

对于接收反应部分,您实现Subscribe方法来询问subscriber实例(再次,一次且仅一次),找出其所有可以"处理"命令的方法。您可以在处理程序中提出一些IHandle<T> where T: ICommand接口,或者只是按照约定(任何只接受一个ICommand参数并返回voidHandle方法)或任何适合您的方法。

它基本上与您所说的"委托/操作"列表的相同部分,只是现在它是每个命令:

map<CommandType, List<action>>

由于所有操作现在只接受一个参数(即ICommand),因此您可以轻松地将它们全部保留在同一列表中。

收到某些命令时,IBus实现仅获取给定命令类型的操作列表,并简单地调用这些操作,将给定命令作为参数传递。

希望对您有所帮助。

高级:您可以更进一步:有一个ConsumeItem命令:

public sealed void ConsumeItem: ICommand {
    public InventoryItem Item { get; set; }
}

您已经有一个负责保存 InventoryItem 和 Command 之间的映射的类,因此此类可以成为进程管理器

  1. 它订阅ConsumeItem命令(通过总线)
  2. 在其Handle方法中,它获取给定库存项的命令列表
  3. 它将这些命令发送到总线。

好了,现在我们已经清楚地区分了这三个问题:

  1. 在使用库存物品时,我们只是"知道"IBus并发送ConsumeItem命令,我们不关心接下来会发生什么。
  2. "ConsumptionInventoryManager"(不管你怎么称呼它)也知道IBus', subscribes for ConsumptionItem'命令,并"知道"当每个项目被消耗时需要做什么(命令列表)。它只是发送这些命令,并不关心谁以及如何处理它们。
  3. 业务逻辑(角色、怪物等)只处理对它们有意义的命令(RestoreHealthDie等),而不关心它们来自哪里(以及为什么)。

祝你好运:)

我的建议是使用反射,即定义一个基于指定名称调用所需方法的方法。下面是一个工作示例:

class Program
{
    static void Main(string[] args)
    {
        SomeClass someInstance = new SomeClass();
        string name = Console.ReadLine();
        someInstance.Call("SayHello", name);
    }
}
class SomeClass
{
    public void SayHello(string name)
    {
        Console.WriteLine(String.Format("Hello, {0}!", name));
    }
    public void Call(string methodName, params object[] args)
    {
        this.GetType().GetMethod(methodName).Invoke(this, args);
    }
}

只要满足以下条件,您就可以这样做:

  1. 您绝对确定可以调用,即存在指定名称的方法,并且参数的数量和类型是正确的

  2. 指定名称的方法不重载,否则会得到System.Reflection.AmbiguousMatchException

  3. 存在一个超类,要
  4. 在其上使用 Call 方法的所有类都派生自该超类;您应该在该类中定义此方法

确保*条件 1. 和 2. 满意 您可以使用更具体的Type.GetMethod版本,它不仅考虑方法的名称,还考虑参数的数量和类型,并在调用之前检查是否存在这样的方法; 然后Call方法将如下所示(*它不适用于参数标记为outref的方法):

public void Call(string methodName, params object[] args)
{
    //get the method with the specified name and parameter list
    Type[] argTypes = args.Select(arg => arg.GetType()).ToArray();
    MethodInfo method = this.GetType().GetMethod(methodName, argTypes);
    //check if the method exists and invoke it
    if (method != null)
        method.Invoke(this, args);
}

备注MethodInfo.Invoke方法实际上返回一个object因此您可以通过指定返回类型并使用return关键字以及适当的强制转换或其他将结果转换为所需类型的方法(如果可能)来定义Call方法来返回某个值 - 请记住检查它是否是。

如果不满足条件 3.,我会继续编写扩展方法。下面是一个返回泛型值的扩展方法的示例,我认为在大多数情况下应该足够了(同样,它不适用于refout),并且应该适用于 .NET Framework 中几乎所有可能的对象(我很感激指出一个反例):

public static class Extensions
{
    //invoke a method with the specified name and parameter list
    // and return a result of type T
    public static T Call<T>(this object subject, string methodName, params object[] args)
    {
        //get the method with the specified name and parameter list
        Type[] argTypes = args.Select(arg => arg.GetType()).ToArray();
        MethodInfo method = subject.GetType().GetMethod(methodName, argTypes);
        //check if the method exists
        if (method == null)
            return default(T); //or throw an exception
        //invoke the method and get the result
        object result = method.Invoke(subject, args);
        //check if something was returned
        if (result == null)
            return default(T); //or throw an exception
        //check if the result is of the expected type (or derives from it)
        if (result.GetType().Equals(typeof(T)) || result.GetType().IsSubclassOf(typeof(T)))
            return (T)result;
        else
            return default(T); //or throw an exception
    }
    //invoke a void method more conveniently
    public static void Call(this object subject, string methodName, params object[] args)
    {
        //invoke Call<object> method and ignore the result
        subject.Call<object>(methodName, args);
    }
}

然后,您应该能够使用例如someObject.Call<string>("ToString")而不是someObject.ToString() 。最后,在这一点上,我强烈建议:

  1. 如果可能,请使用比object更具体的类型

  2. 使用一些比Call更复杂和唯一的名称 - 如果某些类定义了具有相同签名的方法,它可能会被掩盖

  3. 查找协方差逆变以获得更有用的知识