为字符串调用适当的方法
本文关键字:方法 字符串 调用 | 更新日期: 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);
您将如何将方法映射到它们的文件内字符串对应项?这是我的想法:
每次使用项目时,都会将字符串列表(事件)传递给项目事件管理器。管理器分析每个字符串并调用相应的方法。非常容易设置,并且由于这不是一个经常发生的操作,因此对性能的影响可能不大(字符串的大小也很小(最多 10-15 个字符),并在 O(n) 时间内解析)。
每个清单项(类)在初始化时解析字符串事件一次,并且只分析一次。每个字符串事件都通过字典映射到其相应的方法。就性能而言,这是我能想到的最有效的方法,但它使执行其他操作变得非常困难:字典中的所有值都必须是相同类型的委托。这意味着我无法保留
a) 恢复健康(int)
b) 召唤怪物(位置,计数)
在同一字典中,并且必须为每种可调用方法设置新的数据结构。这是一项巨大的工作要做。
想到了一些方法来改进这两种方法:
我可以在 Item 事件中使用某种临时缓存管理器,以便不会解析项目的 OnConsumption 事件两次?我可能会遇到与我在 2 期间遇到的问题相同的问题)但是,由于缓存必须是
map<InventoryItem,List<delegate>>
..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)]
可以映射到RestoreHealth
和RestoreFatigue
命令类,这些命令类可以定义为:
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
参数并返回void
的Handle
方法)或任何适合您的方法。
它基本上与您所说的"委托/操作"列表的相同部分,只是现在它是每个命令:
map<CommandType, List<action>>
由于所有操作现在只接受一个参数(即ICommand
),因此您可以轻松地将它们全部保留在同一列表中。
收到某些命令时,IBus
实现仅获取给定命令类型的操作列表,并简单地调用这些操作,将给定命令作为参数传递。
希望对您有所帮助。
高级:您可以更进一步:有一个ConsumeItem
命令:
public sealed void ConsumeItem: ICommand {
public InventoryItem Item { get; set; }
}
您已经有一个负责保存 InventoryItem 和 Command 之间的映射的类,因此此类可以成为进程管理器:
- 它订阅
ConsumeItem
命令(通过总线) - 在其
Handle
方法中,它获取给定库存项的命令列表 - 它将这些命令发送到总线。
好了,现在我们已经清楚地区分了这三个问题:
- 在使用库存物品时,我们只是"知道"
IBus
并发送ConsumeItem
命令,我们不关心接下来会发生什么。 - "ConsumptionInventoryManager"(不管你怎么称呼它)也知道
IBus', subscribes for
ConsumptionItem'命令,并"知道"当每个项目被消耗时需要做什么(命令列表)。它只是发送这些命令,并不关心谁以及如何处理它们。 - 业务逻辑(角色、怪物等)只处理对它们有意义的命令(
RestoreHealth
、Die
等),而不关心它们来自哪里(以及为什么)。
祝你好运:)
我的建议是使用反射,即定义一个基于指定名称调用所需方法的方法。下面是一个工作示例:
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);
}
}
只要满足以下条件,您就可以这样做:
您绝对确定可以调用,即存在指定名称的方法,并且参数的数量和类型是正确的
指定名称的方法不重载,否则会得到
System.Reflection.AmbiguousMatchException
存在一个超类,要在其上使用
Call
方法的所有类都派生自该超类;您应该在该类中定义此方法
确保*条件 1. 和 2. 满意 您可以使用更具体的Type.GetMethod
版本,它不仅考虑方法的名称,还考虑参数的数量和类型,并在调用之前检查是否存在这样的方法; 然后Call
方法将如下所示(*它不适用于参数标记为out
或ref
的方法):
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.,我会继续编写扩展方法。下面是一个返回泛型值的扩展方法的示例,我认为在大多数情况下应该足够了(同样,它不适用于ref
或out
),并且应该适用于 .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()
。最后,在这一点上,我强烈建议:
如果可能,请使用比
object
更具体的类型使用一些比
Call
更复杂和唯一的名称 - 如果某些类定义了具有相同签名的方法,它可能会被掩盖查找协方差和逆变以获得更有用的知识