本文系个人原创,如转载,请标明出处。由于个人技术有限,如有错误,欢迎大家指出。
如果大家使用了AutoCAD,那么大家就一定知道CAD的命令。在CAD中,命令可以是任意的大小写,也可以是被调用方法的全名或者缩写形式。那么这个命令系统是如何实现的呢?
笔者经过一番研究,总结出了如下的四种方式。
首先我们定义一个Commander类,这个类的成员只有方法,这些方法都是有可能被调用的。在此,笔者假定这个类只有三个方法。
static class Commander
{
static public void Say()
{
Console.WriteLine("Say what? ");
}
static public void Ipad()
{
Console.WriteLine("yes i have a ipad.");
}
static public void Go()
{
Console.WriteLine("where are we going?");
}
}
第一种方式:使用switch……case
这种方式比较简单。用一个字符串保存用户输入的命令,然后通过switch……case方式调用方法:
class Program
{
static void Main(string[] args)
{
do
{
Console.WriteLine("请输入命令...");
string cmd = Console.ReadLine().ToLow();
switch (cmd)
{
case "g":
case "go":
{
Commander.Go();
break;
}
case "s":
case "say":
{
Commander.Say();
break;
}
case "i":
case "ipad":
{
Commander.Ipad();
break;
}
}
} while (true);
}
}
如果需要调用的方法不是很多,且调用的方法数量固定不变,那么可以使用switch…case…语句来判断用户输入的是什么。这种方式简单,高效。但是如果需要添加被调用的方法,那么就需要改动switch…case…结构。这样很不利于程序的扩展。
第二种方式:使用Dictionary<>
C#中的Dictionary<>泛型类用于保持一组键—值关系,其中每一个键对应一个值。那么在这个项目中,我们就可以把用户调用方法的命令设置为键,把方法作为值来处理。
由于Dictionary<>类只能保存键与值的对应关系,而不能是键与方法的对应关系,所以被调用的方法必须有一个返回值。为了使用同一个Dictionary<>对象,被调用的所有方法都需要有同样的返回类型。在这个示例中,我们定义键为string类型,值为int类型。
首先,我们将Commander类做如下修改:
class Commander
{
static public int Say()
{
try
{
Console.WriteLine("Say what? ");
}
catch
{
return 0; //如果执行出错,就返回0
}
return -1; //如果执行成功,就返回1
}
static public int Ipad()
{
try
{
Console.WriteLine("yes i have a ipad.");
}
catch
{
return 0;
}
return -1;
}
static public int Go()
{
try
{
Console.WriteLine("where are we going?");
}
catch
{
return 0;
}
return -1;
}
}
然后我们需要在程序中定义一个Dictionary<>的集合,把所有的方法都添加到这个集合对象中:
class Program
{
static void Main(string[] args)
{
Dictionary<string, int> meths=new Dictionary<string,int>;
meths.Add("s",Commander.Say());
meths.Add("say",Commander.Say());
meths.Add("i",Commander.Ipad());
meths.Add("ipad",Commander.Ipad());
meths.Add("g",Commander.Go());
meths.Add("go",Commander.Go());
do
{
Console.WriteLine("请输入命令...");
string cmd=Console.ReadLine().ToLow();
int Result=meths[cmd]; //通过键,查找该键对应的值。
}while(true);
}
}
在这种方式下,程序可接受的命令就是Dictionary<string, int>类中的string类型的值。注意,在C#中,Dictionary<>的键只能是唯一的,但是值可以不唯一,也就是说多个不同的键可以对应同一个值。这样一来,只要能想到的命令,都可以添加到这个集合对象中。
这种方式的调用效率非常高,相比第一种方式,代码更简单,并且可以通过返回值来判断方法是否调用成功。
但是同第一中方式一样,如果我们需要在后期扩展方法的话,那么就必须手动改写这个Dictionary<string, int>的对象,以添加对方法调用。这样一来,做二次开发的时候就比较困难了。
第三种方式:使用反射
使用反射的基本思想是将某个类型抽象化,并且获取这个类型所有成员。例如上面的Commander类型,就可以将其抽象化,然后通过GetMember()方法获取Commander类的所有的成员,包括方法,字段,属性,事件等。
在C#中,抽象化某个类,需要使用Type类。Type类是一个抽象类,不能直接实例化。但是C#的编译器会自动创建Type类的子类,用这个子类来创建对象。通常而言,我们不会对Type类进行改写。所以编译器运行我们省略创建子类的过程,而直接使用Type类。如下所示:
<span style="font-size:12px;">Type TCommander;
</span>
在C#中要抽象化某个类,可以使用typeof运算符,或者某个类的GetType()方法。所以我们使用某个对象来抽象化Commander类时,有如下两种方式:
Type TCommander=typeof(Commander);
Type TCommander=Commander.GetType();
一旦获得了这个抽象对象,我们就可以使用各种Get**()方法获取这个类的成员。关于Type类的获取成员的方法,在这里笔者就不一一介绍了。现在我们可以使用GetMethods()来获取Commander类的方法数组。
注意:这里所说的方法数组,并不是什么特殊名字,只不过是将所有的方法列在一起,成为一个数组。所以,每个方法都是数组的一个元素而已。
首先,我们使用第一种方式下定义的Commander类。然后在Main()方法中写如下代码:
class Program
{
static void Main(string[] args)
{
//获取Commander类的抽象化对象。
Type TCommander = typeof(Commander);
//使用Type类型的GetMethods()方法获取Commander类的方法数组。
//也可以使用GetMethod(string name)方法获取指定方法名字的方法。例如:
//MethodInfo meths = TCommander.GetMethods("Say");//获取名字为Say的方法。
MethodInfo[] meths = TCommander.GetMethods();
//存储用户输入的命令
string cmd;
do
{
Console.WriteLine("请输入命令的全名.");
cmd = Console.ReadLine();
//遍历数组,如果用户输入的命令与数组中方法的名字相同(忽略大小写),就调用这个方式,并退出循环
for (int i = 0; i < meths.Length; i++)
{
if (cmd.Equals(meths[i].Name, StringComparison.OrdinalIgnoreCase))
{
//调用meths引用的对象中所保持的方法,使用invoke方法。
//如果方法是静态类的静态方法,那么第一参数object就应该是null,如果方法是实例类的,那么就应该第一个参数object就应该是这个实例类的对象,
//第二个参数是params object[] obj,它是指方法中需要用到的参数。如果方法有参数,那么在调用invoke方法时,就应该把方法用到的参数按顺序写在invoke参数列表中。如果被调用的方法没有参数,那么invoke中的第二个参数应该是null。
//本例中,Commander类是静态类,不存在实例对象,所有invoke的第一个参数是null,所调用的方法没有参数,所有invoke中的第二个参数也是null。
meths[i].Invoke(null, null);
break;
}
}
Console.WriteLine("您输入的命令有误,请重新输入...");
} while (true);
}
}
这种方式实现了动态获取方法。也就是说,在将来做二次开发的时候,可以直接扩展Commander类,而不需要改动Commander类源代码。并且,这种方式也实现了不论用户输入的大小写形式是否正确,只要输入的是方法的全名,就可以调用方法。但是他却没有实现用缩写命令调用方法,而且,如果Commander类有成千上万个方法,那么循环这个数组,势必会造成性能的降低。
第四种方式:使用特性
C#中的特性(Attribute)是用来描述元数据的方式。所谓的元数据,是指程序集中的内部成员。那么特性就是用来描述这些成员的。例如在方法上使用一个特性obsolete,就告诉编译器这个方法是过时的。如下所示:
[Obsolete]
static public void Go()
{
//......
}
C#运行时(Runtime)可以识别特性。那么我们就可以给方法添加特定的特性,然后运行的时候,通过命令调用指定特性的方法。
要实现这样的功能,我们就需要先定义一个自己的特性类,并使用某个字段来存储可以调用该方法的命令。如下所示:
[AttributeUsage(AttributeTargets.Method)] //指定该特性(CmdAttribute)的应用目标只能是方法(Method)
{
string[] cmd;
public string[] Cmd
{
get { return cmd; }
}
public CmdAttribute(params string[] cmd) //参数数组,将命令存入cmd数组。
{
this.cmd= cmd;
}
}
现在我们需要改写一下Commander类,将我们的特性应用到方法上。
static class Commander
{
[CmdAttribute("say","s")] //使用参数“say”、“s”实现多个命令调用同一个方法。
static public void Say()
{
Console.WriteLine("Say what? ");
}
[CmdAttribute("ipad", "i")]
static public void Ipad()
{
Console.WriteLine("yes i have a ipad.");
}
[CmdAttribute( "go", "g")]
static public void Go()
{
Console.WriteLine("where are we going?");
}
}
现在需要被调用的方法已经定义好了,那么我们就需要实现调用方法了。如下代码所示:
class Program
{
static void Main(string[] args)
{
//存储用户输入的命令
string cmd;
CmdAttribute attr;
Type TCommand = typeof(Commander);
Type TCmdAttribute = typeof(CmdAttribute);
MethodInfo[] meths = TCommand.GetMethods();
//第一层循环,用于连续接受用户的输入
do
{
//这个标签用于goto语句退出两层循环
BreakOut:
Console.WriteLine("请输入命令...");
cmd = Console.ReadLine().ToLower();
//第二层循环,用户循环方法数组meths
foreach(MethodInfo M in meths)
{
//判断方法是否应用了TCmdAttribute所抽象的特性。并且不向上查找继承的类。
//这一句非常重要。因为Type的GetMethods()方法会获取类的所有方法,包括他所继承的基类的方法。
//而基类的方法是没有使用我们自定义的特性,那么在进行下一句,对attr赋值是,attr会是一个空。
//所有我们使用MethodInfo对象的IsDefined()方法来确定某个方法使用了某个自定义的特性。
if (M.IsDefined(TCmdAttribute, false))
{
//获取方法的特性。
attr = (CmdAttribute)M.GetCustomAttribute(TCmdAttribute, false);
//第三层循环,确定某个方法的特性的Cmd属性中的命令,与用户输入的命令是否匹配。
//如果两者相匹配,就调用这个命令,然后退出最后两层循环,继续等待用户输入命令。
//如果查找完整个数组都没有与用户输入的命令相匹配的方法特性,就提示用户输入的命令有误。
foreach (string commad in attr.Cmd)
{
if (cmd == commad)
{
M.Invoke(null, null);
//跳出最后两层循环,并继续执行第一层循环。
goto BreakOut;
}
}
}
}
Console.WriteLine("你输入的命令有误,请重新输入...");
} while (true);
}
}
第四种方式很好的解决了两个问题。第一是关于程序的扩展。只要通过扩展Commander类,就可以添加更多的功能。第二是关于调用方法的命令。只要在扩张Commander类的同时,在方法上加入特性[CmdAttribute("***","***","**")],并在特性中指定希望用什么字符串可以调用这个方法,就可以很方便的调用方法了。
注意:在上例的代码中有这么一句:
MethodInfo[] meths = TCommand.GetMethods();
这行代码用于返回TCommander对象所抽象的Commander类的方法数组。这个数组中包含了Commander类的所有方法,包括它自己,也包括它所继承的。所以在后面的代码中,笔者使用了一句
if (M.IsDefined(TCmdAttribute, false))
来过滤掉没有使用特性的方法。
然而,事实上,在C#中是可以通过bindingFlags参数来指定需要获取的方法的。
如下所示:
MethodInfo[] meths = TCommand.GetMethods(bindingAttr:BindingFlags.DeclaredOnly);
但是在笔者测试的过程中,这条语句一直不能按照正常意图执行成功,而必须使用如下形式才能获取我想要的方法数组:
<span style="font-family:SimSun;font-size:12px;">MethodInfo[] meths = TCommand.GetMethods(BindingFlags.DeclaredOnly|BindingFlags.Static|BindingFlags.Public);</span>
对此,我也没太明白为什么微软非得要我们这么写。
至此,笔者列出了四种实现CAD命令系统的方法。从方便使用的角度来说,第四种方式是最好的,他既实现了多个命令调用同一方法,又实现了动态加载方法,极大的方便了程序的扩展。但就运行效率而言,可能就有点差了。这得等到笔者把Commander类扩大之后才能测试了。
如果大家有什么更好的方法,欢迎指出,大家一起讨论。