如何实现类似CAD的命令系统

本文系个人原创,如转载,请标明出处。由于个人技术有限,如有错误,欢迎大家指出。


如果大家使用了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 meths=new Dictionary;
            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类型的值。注意,在C#中,Dictionary<>的键只能是唯一的,但是值可以不唯一,也就是说多个不同的键可以对应同一个值。这样一来,只要能想到的命令,都可以添加到这个集合对象中。

这种方式的调用效率非常高,相比第一种方式,代码更简单,并且可以通过返回值来判断方法是否调用成功。

但是同第一中方式一样,如果我们需要在后期扩展方法的话,那么就必须手动改写这个Dictionary的对象,以添加对方法调用。这样一来,做二次开发的时候就比较困难了。


第三种方式:使用反射

使用反射的基本思想是将某个类型抽象化,并且获取这个类型所有成员。例如上面的Commander类型,就可以将其抽象化,然后通过GetMember()方法获取Commander类的所有的成员,包括方法,字段,属性,事件等。

在C#中,抽象化某个类,需要使用Type类。Type类是一个抽象类,不能直接实例化。但是C#的编译器会自动创建Type类的子类,用这个子类来创建对象。通常而言,我们不会对Type类进行改写。所以编译器运行我们省略创建子类的过程,而直接使用Type类。如下所示:

Type TCommander;


在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);

但是在笔者测试的过程中,这条语句一直不能按照正常意图执行成功,而必须使用如下形式才能获取我想要的方法数组:

MethodInfo[] meths = TCommand.GetMethods(BindingFlags.DeclaredOnly|BindingFlags.Static|BindingFlags.Public);

对此,我也没太明白为什么微软非得要我们这么写。


至此,笔者列出了四种实现CAD命令系统的方法。从方便使用的角度来说,第四种方式是最好的,他既实现了多个命令调用同一方法,又实现了动态加载方法,极大的方便了程序的扩展。但就运行效率而言,可能就有点差了。这得等到笔者把Commander类扩大之后才能测试了。


如果大家有什么更好的方法,欢迎指出,大家一起讨论。



你可能感兴趣的:(我的学习笔记)