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 (