笔者的上一片文章中用了四种方式来实现CAD命令系统的功能,也提到了各自的优缺点,也提出了自己的观点,使用第四种方式能获得非常好的扩展性体验。然而,仔细观察一下代码,读者会发现,第四种实现方法是通过三层循环(第一层循环是为了连续读取用户输入的命令。),将用户输入的命令与方法的特性值一个一个做比对而实现的。也就是说,在用户输入完命令后,程序就开始遍历方法数组,然后获得某个方法的特性,然后再遍历这个方法的特性,并将特性值与用户输入的命令做比较。
如果方法的数量不多,且方法的特性性比较少时,这么做不会有太大的问题。但是对于一个CAD系统,被调用的方法有上千个,而每个方法又有多个不同的命令。那么要查找到需要的命令,可能就需要循环上万次。这绝对是件耗费时间、考验机器性能的事情。那么有什么方法能更好的解决这个问题呢?
在上一篇文章中笔者提到过字典类。字典类的运算速度非常快,因为他使用了散列算法,可以在微秒级别上迅速找出给定的键的值。既然它的速度这么快,有没有办法把反射和字典这两种方法结合起来呢?
答案是肯定的,关键点就在MehodInfo类!
在上一篇文章中,笔者提到了反射和特性中有一个MethodInfo的类。这个类在反射中是表示方法的元数据,说的通俗点,它就表示一个方法,只不过这个被表示的方法只能通过MethodInfo类的Invoke方法被调用。既然MehodInfo的对象代表的是方法,那么我们就可以定义一个“命令—方法”键值关系,并保存在字典类中。用“命令”做为“键”,用“方法”作为值。
ok,下面来看代码。由于在这个程序中,我们需要考虑扩展以及多命令,所以还是要使用“特性”和“反射”等技术。
首先,定义一个特性类和命令类,并在命令类中的方法上使用特性。
/// <summary>
/// 用于反射的特性
/// </summary>
class CMDAttribute : System.Attribute
{
string[] cmd;
public string[] Cmd
{
get { return cmd; }
}
public CMDAttribute(params string[] cmd)
{
this.cmd = cmd;
}
}
/// <summary>
/// 这是个命令类,供用户调用
/// </summary>
static class Commander
{
[CMD("s","sh","h","hi")]
static public int SayHi()
{
Console.WriteLine("Hello girl. How beautifull you are!");
return 0;
}
[CMD("i","ip","ipad")]
static public int Ipad()
{
Console.WriteLine("yes i have a Ipad!");
return 0;
}
}
注意:Commander类的定义中使用了static关键字,主要是为了本例的方便。因为如果不使用static关键字,那么在使用这个类的时候,还必须先创建对象。
现在我们再定义一个类,让这个类的对象被创建的时候,自动加载保存“命令—方法”的字典。代码如下:
class InitilizeCmd
{
Dictionary<string, MethodInfo> cmd;
public Dictionary<string, MethodInfo> Cmd
{
get { return cmd; }
}
public void InitilizeCmd()
{
CMDAttribute Attri;
Type TPC = typeof(Commander);
Type TPA = typeof(CMDAttribute);
MethodInfo[] meths = TPC.GetMethods();
//遍历方法数组,获取每个方法的特性,然后将方法的特性与用户输入的命令比较。如果用户输入的命令与特性一致,就执行这个特性所表示的方法,如果不一致,就告诉用户命令有错。
foreach (MethodInfo M in meths)
{
//判断方法是否应用了TCmdAttribute所抽象的特性。并且不向上查找继承的类。
//这一句非常重要。因为Type的GetMethods()方法会获取类的所有方法,包括他所继承的基类的方法。
//而基类的方法是没有使用我们自定义的特性,那么在进行下一句,对attr赋值是,attr会是一个空。
//所有我们使用MethodInfo对象的IsDefined()方法来确定某个方法使用了某个自定义的特性。
if (M.IsDefined(TPA, false))
{
//获取方法的特性。
Attri = (CMDAttribute)M.GetCustomAttribute(TPA, false);
//将方法的特性和方法名作为一个键—值关系存入Dictionary<string, int>字典中。
foreach (string commad in Attri.Cmd)
{
cmd.Add(commad, M);
}
}
}
}
}
到目前为止,我们的命令类有了,利用特性和反射将“命令(特性值)”和“方法”保持至字典的类也有了。现在要使用这个类,只需要创建InitilizeCmd类的对象就可以了。
那么在程序的Main()函数中我们就可以这样写了:
static void Main(string[] args)
{
//创建InitilizeCmd类的对象,并初始化。
InitilizeCmd INCMD = new InitilizeCmd();
//创建MethodInfo对象
MethodInfo M;
do
{
Console.WriteLine("请输入命令...");
string InputCMD = Console.ReadLine().ToLower();
try
{
M = INCMD.Cmd[InputCMD];
M.Invoke(null, null);
}
catch (Exception e)
{
Console.WriteLine("命令输入有误,请重新输入\n"+e.Message);
}
} while (true);
}
在Main()函数中使用try……catch结构是因为如果用户输入的命令不正确,会导出无法查找字典中键—值,使得程序出错。
此致,将“字典”与“特性”、“反射”相结合的方法就介绍完了。这样做的好处显而易见,只在加载程序时利用反射遍历一次方法数组,而无需在输入命令时一遍又一遍的遍历数组。这样既保证了程序的扩展性,也保证了大大提高了程序运行的性能。