CodeDom计算器——动态计算数学表达式的实现

序言:前几天整理资料时发现以前翻译的一篇关于CodeDom的文章,虽然题材比较老了,但还是可能对部分兄弟有用,贴出来与大家共享,不妥之处敬请指出。原文来自CSharpCornerhttp://www.c-sharpcorner.com/UploadFile/mgold/CodeDomCalculator08082005003253AM/CodeDomCalculator.aspx

介绍:

借助CodeDomReflection我们可以动态编译C#代码并使之在程序中任何地方都能运行。这个强大的特性允许我们创建在Windows窗体甚至C#代码行中都可以运算数学表达式的CodeDom计算器。首先我们需要借助System.Math类来进行计算,但我们并不需要在算式前加上Math. 前缀,下面将向您展现CodeDom计算器是如何实现的。 


1 – 运行中的CodeDom计算器

 

用法:

CodeDom可依下列两种方法使用:

1、用C#语法输入你要计算的数学表达式

2、写一个计算复杂算式的C#代码块

第一种方法仅需要按图2中的方法输入算式即可。 


2 – CodeDom计算器中运算一个较长的算式

在第二种方法中我们实现的手段略有差异。最上面一行写一个answer加上分号,之后你就可以写任何C#代码,在代码片断的末端将你需要得到的答案赋予answer变量。在写代码时同上也不用加Math类前缀,图3是用CodeDom求从110的和的示例: 


3 – CodeDom计算从110的和

 

创建并运行Calculator

运算表达式的下面几步是:

1、使用CodeDom依据算式创建C#代码

2、使用CodeDom编译器将这段代码编译为程序集

3、创建一个Calculator类的实例

4、调用Calculator类中的Calculate方法得到答案

下表就是我们要创建的CodeDom类,Calculate方法将包含我们输入CodeDom计算器的数学表达式。 


4 – UML反工得到的Calculator

事实上图3中用到的CodeDom程序集是由下列代码创建的。下一节我们将讲述更多关于如何创建包含CodeDom所有方法的类,这些方法真是酷毙了。真如您所见,我们的表达式被传入Calculate方法中。我们需要将answer;放到第一行是为了在Calculate方法强制置入一个哑元行来引入较大的代码块(这个哑元行是Answer = answer;)。如果我们输入一个简单的表达式如11,在代码内将产生一行Answer = 1 + 1;

列表 1 – CodeDom为计算器产生的代码

 namespace ExpressionEvaluator {

    using System;
    using System.Windows.Forms;

    public class Calculator
    {       
        private double answer;       

        /// Default Constructor for class
        public Calculator()
        {
            //TODO: implement default constructor
        }

       

        // The Answer property is the returned result
        public virtual double Answer
        {
            get
               {
                return this.answer;
               }

            set
              {
                this.answer = value;
              }
        }

        /// Calculate an expression
        public virtual double Calculate()
        {
          Answer = answer;
          for (int i = 1; i <= 10; i++)
              answer = answer + i;

            return this.Answer;
        }

    }
}

代码分析

点击计算按钮后代码产生、编译、运行。  列表2 展示了按顺序执行这几步的calculate event handler。尽管这不是全部的代码,所有的步骤已经在BuildClass, CompileAssemblyRunCode方法里全部包括:.

列表 2 – 计算数学表达式的Event Handler

            private void btnCalculate_Click(object sender, System.EventArgs e)
            {
            // Blank out result fields and compile result fields
            InitializeFields(); 

            // change evaluation string to pick up Math class members
           string expression = RefineEvaluationString(txtCalculate.Text);

            // build the class using codedom
            BuildClass(expression);

             // compile the class into an in-memory assembly.
            // if it doesn't compile, show errors in the window
            CompilerResults results = CompileAssembly();

            // write out the source code for debugging purposes
            Console.WriteLine("...........................\r\n");
            Console.WriteLine(_source.ToString());

 

            // if the code compiled okay,
            // run the code using the new assembly (which is inside the results)
            if (results != null && results.CompiledAssembly != null)
            {
                // run the evaluation function
                RunCode(results);
            }
        }


CodeDom
看起来怎么样呢如果你仔细观察过CodeDom中的类, 就会发现它们几乎就是违反语法的。每个构造器使用其他CodeDom对象来构造自己并构造其他语法片断的合成物。表1展示了我们在这个工程中构造程序集用到的所有类和他们各自的用途。

CodeDom 对象

用途

CSharpCodeProvider

生成C#代码的Provider

CodeNamespace

构造名称空间的类

CodeNamespaceImport

创建调用申明

CodeTypeDeclaration

创建类结构

CodeConstructor

创建构造器

CodeTypeReference

创建一个类型的引用

CodeCommentStatement

创建C#注释

CodeAssignStatement

创建委派申明

CodeFieldReferenceExpression

创建一个field引用

CodeThisReferenceExpression

创建一个this指针

CodeSnippetExpression

创建一个在代码中指定的文字字符串 (用于放置表达式)

CodeMemberMethod

创建一个新的方法

1 – 构建计算器需要用到的CodeDom

让我们看看列表3中用来生成代码的CodeDom方法。大家可以看到用CodeDom生成代码是比较容易的,因为它把复杂的代码生成工作分割成了几个简单的部分。我们先创建一个生成器,在本例中我们要生成C#代码所以创建了C#生成器;然后开始创建并装配各个部分。首先创建命名空间,然后添加导入我们需要的各个类库,其次创建类,给类添加一个构造器一个属性和一个方法。在这个方法里,我们添加了方法的声明,声明中连入文本框输入的要求值的运算表达式CodeSnippetExpression构造器中使用输入的运算表达式这样我们就能从赋值字符串中直接生成代码。这个表达式也使用了CodeAssignStatement构造器,这样我们就能将其分配给Answer属性。当我们完成装配CodeDom各个层次的组成部分后,只需要用已装配命名空间的CodeDom构造器来调用GenerateCodeFromNamespace即可。由它输出字符串流到StringWriter并内部指派一个可以直接从字符串中释放全部代码集合的StringBuilder

列表 3 – 使用CodeDom类构造Calculator

/// <summary>

            /// Main driving routine for building a class

            /// </summary>
        void BuildClass(string expression)
         {
            // need a string to put the code into
              _source = new StringBuilder();

              StringWriter sw = new StringWriter(_source);

            //Declare your provider and generator
              CSharpCodeProvider codeProvider = new CSharpCodeProvider();
              ICodeGenerator generator = codeProvider.CreateGenerator(sw);                 
              CodeGeneratorOptions codeOpts = new CodeGeneratorOptions();
              CodeNamespace myNamespace = new CodeNamespace("ExpressionEvaluator");

              myNamespace.Imports.Add(new CodeNamespaceImport("System"));
              myNamespace.Imports.Add(new CodeNamespaceImport("System.Windows.Forms"));

 

             //Build the class declaration and member variables               
             CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration();

                  classDeclaration.IsClass = true;
                  classDeclaration.Name = "Calculator";
                  classDeclaration.Attributes = MemberAttributes.Public;
                  classDeclaration.Members.Add(FieldVariable("answer", typeof(double), MemberAttributes.Private));

 

                  //default constructor
                  CodeConstructor defaultConstructor = new CodeConstructor();
                  defaultConstructor.Attributes = MemberAttributes.Public;
                  defaultConstructor.Comments.Add(new CodeCommentStatement("Default Constructor for class", true));
                  defaultConstructor.Statements.Add(new CodeSnippetStatement("//TODO: implement default constructor"));
                  classDeclaration.Members.Add(defaultConstructor);

 

                  //home brewed method that uses CodeDom to make a property
                  classDeclaration.Members.Add(this.MakeProperty("Answer", "answer", typeof(double)));

                   //Our Calculate Method
                  CodeMemberMethod myMethod = new CodeMemberMethod();

                  myMethod.Name = "Calculate";
                  myMethod.ReturnType = new CodeTypeReference(typeof(double));
                  myMethod.Comments.Add(new CodeCommentStatement("Calculate an expression", true));
                  myMethod.Attributes = MemberAttributes.Public;
                  myMethod.Statements.Add(new CodeAssignStatement(new CodeSnippetExpression("Answer"),
                        new
CodeSnippetExpression(expression)));

//            Include the generation below if you want your answer to pop up in a message box
//            myMethod.Statements.Add(new CodeSnippetExpression("MessageBox.Show(String.Format(\"Answer = {0}\", Answer))"));

            //  return answer
            myMethod.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(
                  new
CodeThisReferenceExpression(), "Answer")));

                  classDeclaration.Members.Add(myMethod);

                  //write code
                  myNamespace.Types.Add(classDeclaration);
                  generator.GenerateCodeFromNamespace(myNamespace, sw, codeOpts);
                 
                  // cleanup
                  sw.Flush();
                  sw.Close();
            }

编译

编译被分解为3个部分: 创建CodeDom编译器,创建编译参数,并如列表4所示将代码编译进程序集。

列表 4  - 使用CodeDom编译程序集

      /// <summary>
        /// Compiles the c# into an assembly if there are no syntax errors
        /// </summary>
        /// <returns></returns>

        private CompilerResults CompileAssembly()
        {
            // create a compiler
            ICodeCompiler compiler = CreateCompiler();
            // get all the compiler parameters
            CompilerParameters parms = CreateCompilerParameters();
            // compile the code into an assembly
            CompilerResults results = CompileCode(compiler, parms, _source.ToString());

            return results;
        }

CreateCompiler代码创建C# CodeDom provider对象并从其中创建一个编译器对象。

列表 5 – 创建C#编译器对象

ICodeCompiler CreateCompiler()
        {
            //Create an instance of the C# compiler  
            CodeDomProvider codeProvider = null;
            codeProvider = new CSharpCodeProvider();
           ICodeCompiler compiler = codeProvider.CreateCompiler();
            return compiler;
        }

如列表6所示,我们需要将编译器参数放到一起。我们还需要设定适当的编译器选项在内存中生成dll类库,我们也可以使用这些参数来添加各种引用库到包含System.Math类的系统库中。

列表 6 – 为编译器创建参数

            /// <summary>

        /// Creawte parameters for compiling

        /// </summary>

        /// <returns></returns>

        CompilerParameters  CreateCompilerParameters()
        {
            //add compiler parameters and assembly references
            CompilerParameters compilerParams = new CompilerParameters();
            compilerParams.CompilerOptions = "/target:library /optimize";
            compilerParams.GenerateExecutable = false;
            compilerParams.GenerateInMemory = true;
            compilerParams.IncludeDebugInformation = false;
            compilerParams.ReferencedAssemblies.Add("mscorlib.dll");
            compilerParams.ReferencedAssemblies.Add("System.dll");
            compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");

            return compilerParams;
        }

最终我们需要编译的是代码。这是用CompileAssemblyFromSource方法完成的,如列表7所示。这个方法提取列表5设定的参数与字符串形式的代码集合并将代码编译为一个程序集。对该程序集的引用将指派给编译器结果。如果编译过程中有错误,我们在地步的文本框中输出并设定编译器结果为null,这样就不用再去尝试编译并运行程序集了。

列表 7 – 使用编译器参数编译创建的代码生成程序集

        private CompilerResults CompileCode(ICodeCompiler compiler, CompilerParameters parms, string source)
        {
                        //actually compile the code

            CompilerResults results = compiler.CompileAssemblyFromSource(
                                        parms, source);

            //Do we have any compiler errors?
 
          if (results.Errors.Count > 0)
            {
              foreach (CompilerError error in results.Errors)
                 WriteLine("Compile Error:"+error.ErrorText);
                return null;
            }   

            return results;
        }

运行代码

如果编译后没有错误,我们可以直接运行程序集,但这一步并不是由CodeDom而是由反射来完成的。反射允许我们在内存中新创建的程序集上创建一个Calculator对象并执行其中的Calculate方法。列表8向我们展示了如何执行Calculate方法。首先从编译结果中取得执行程序集的引用,其次在新创建的Calculator类中调用CreateInstance来构造一个类的实例。遍历程序集中包含的每一个类(本例中只有一个Calculator类)并取得类的定义。然后遍历类中的每个成员寻找Calculate方法。一旦找到Calculate方法,只需要通过新创建的对象CreateInstance简单地调用Calculate方法的Invoke即可。这将执行我们CodeDom生成程序集中的Calculate并返回结果的double值,然后将其显示在结果文本框里。

列表 8 – 通过Reflection运行刚创建程序集的Calculate方法

        private void RunCode(CompilerResults results)
        {
              Assembly executingAssembly = results.CompiledAssembly;

               try
                  {

                    //can't call the entry method if the assembly is null
   
                if (executingAssembly != null)
                     {

                       object assemblyInstance = executingAssembly.CreateInstance("ExpressionEvaluator.Calculator");

                      //Use reflection to call the static Main function                  

                      Module[] modules = executingAssembly.GetModules(false);
                      Type[] types = modules[0].GetTypes(); 

                              //loop through each class and each method that was defined
                             //  and look for the first occurrence of the Calculate method

                       foreach (Type type in types)
                         {
                              MethodInfo[] mis = type.GetMethods();
                              foreach (MethodInfo mi in mis)
                               {
                                  if (mi.Name == "Calculate")
                                  {
                                     // execute the Calculate method, and retrieve the answer
                                     object result = mi.Invoke(assemblyInstance, null);

                                     // place the answer on the win form in the result text box
                                     txtResult.Text =  result.ToString();
                                  }
                               }

                          }

                     }

                  }
                  catch (Exception ex)
                  {
                        Console.WriteLine("Error:  An exception occurred while executing the script", ex);                
                  }

        }

使输入的内容可编译

能够在编译之前决定输入的什么是可编译的并筛选可编译的部分是一件程序运行时动态接受代码的乐事。同理,输入的代码不一定要是C#的,我们只需要在编译前得到C#代码即可,我们决定将CodeDom计算器做的更简单易用,当一个人输入表达式时不一定要输入System.Math 类库的Math前缀。通过对输入字符串的预处理,在需要的地方自动插入Math前缀(当前这些工作应该在编译前完成),我们可以提高输入表达式的可编译性。同时,用户不用担心Math类库的使用场合,我们也可以预处理类似情形。首先使用反射来创建一个Math类库所有成员的图,然后使用正则表达式匹配释放字符串得所有单词并观察其是否是Math类库图中的成员。列表9向大家展示了我们如何通过反射创建一个包含所有Math类成员的图。

列表 9 – 使用Reflection收集到的Math类的成员

        ArrayList _mathMembers = new ArrayList();
        Hashtable _mathMembersMap = new Hashtable(); 

        void GetMathMemberNames()
        {
            // get a reflected assembly of the System assembly
            Assembly systemAssembly = Assembly.GetAssembly(typeof(System.Math));

            try
            {
                //cant call the entry method if the assembly is null
                if (systemAssembly != null)
                {

                    //Use reflection to get a reference to the Math class 

                    Module[] modules = systemAssembly.GetModules(false);
                    Type[] types = modules[0].GetTypes();

                     //loop through each class that was defined and look for the first occurrance of the Math class
                    foreach (Type type in types)
                    {
                        if (type.Name == "Math")
                        {

                            // get all of the members of the math class and map them to the same member
                            // name in uppercase

                            MemberInfo[] mis = type.GetMembers();

                            foreach (MemberInfo mi in mis)
                            {
                                _mathMembers.Add(mi.Name);
                                _mathMembersMap[mi.Name.ToUpper()] = mi.Name;
                            }
                        }

                    }

                }

            }

            catch (Exception ex)
            {
                Console.WriteLine("Error:  An exception occurred while executing the script", ex);
            }

        }

列表10展示了如何使用Math成员图来决定代码中哪里需要加Math前缀使其可编译。通过使用正则字符串类,我们可以按字母顺序查找到所有释放出来的表达式字词,对照Math成员检查这些字词。任何与System.Math成员同名的字词都将被冠以Math前缀与图中的成员名称。因为math成员中的图关键字都是大写的而math成员中得值都是小写得,所以用户输入的表达式不用担心大小写区别问题。无论用户输入的是大写还是小写,释放出的字词都将用之前反射读取出的System.Math类库中的正确形式来替代。

列表 10 - Tweaking the evaluation expression by adding the static Math class prefix

        /// <summary>
        /// Need to change eval string to use .NET Math library
        /// </summary>
        /// <param name="eval">evaluation expression</param>
        /// <returns></returns>
        string RefineEvaluationString(string eval)
        {

            // look for regular expressions with only letters
            Regex regularExpression = new Regex("[a-zA-Z_]+");

            // track all functions and constants in the evaluation expression we already replaced
            ArrayList replacelist = new ArrayList();

            // find all alpha words inside the evaluation function that are possible functions
            MatchCollection matches = regularExpression.Matches(eval);

            foreach (Match m in matches)
            {
                // if the word is found in the math member map, add a Math prefix to it
                bool isContainedInMathLibrary = _mathMembersMap[m.Value.ToUpper()] != null;
                if (replacelist.Contains(m.Value) == false && isContainedInMathLibrary)
                {
                    eval = eval.Replace(m.Value, "Math." + _mathMembersMap[m.Value.ToUpper()]);
                }

                // we matched it already, so don't allow us to replace it again
                replacelist.Add(m.Value);
            }

            // return the modified evaluation string
            return eval;
        }

结论

CodeDom展现给我们一个动态编码成为可能的魔法世界,从此代码编写代码将不再是仅能在科幻小说中看到的故事。除此之外还有其他动态代码生成的实用方法如AOP、动态状态机和强大的脚本引擎。我们将见到这种富有潜力的技术得到更多应用,同时,在进行C#编码时请时刻关注下一代.NET Framework

 

 

posted on 2006-02-11 09:55  Zeus  阅读(4844)  评论(6编辑  收藏  举报

导航