正文

c#实现字符串表达式计算2008-07-26 15:37:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/iamben250/37112.html

分享到:

CodeDom Calculator - Evaluating C# Math Expressions dynamically

This article describes how to use CodeDom and Reflection to Create a Calculator that can evaluate simple and complex math expressions on the fly.

Author Rank:
Technologies: .NET 1.0/1.1, Windows Forms,Visual C# .NET
Total downloads : 1223
Total page views :  34485
Rating :
 5/5
This article has been rated :  6 times
  Add to Technorati Add to Technorati    Digg This Digg This    Add to del.icio.us Add to del.icio.us
    Rate this article Read/Post comments Support Us   Printable Version 

Download Files
CodeDomCalculator.zip
Sponsored by

Looking for a book on Windows Forms? Here is our book
Description
This book is an in-depth treatment of printing functionality available in .NET Framework with real-world ready to use sample code written in .NET 2.0. This book also answers some of the most commonly asked questions such as how to print text files, how to print forms and controls and much more.
Browse more books here»

Introduction

CodeDom and Reflection give you the ability to dynamically build C# Code into a string, compile it, and run it all inside your program. This very powerful feature in .NET allows us to create the CodeDom calculator, a calculator that evaluates expressions (and even lines of C# code) inside a Windows Form. We primarily use the System.Math class to do the calculations, but we've coded the CodeDom calculator in such a way, so that we don't need to apply the Math. prefix before our functions. We'll show you in a minute how this is done.  

 

Figure 1 - CodeDom Calculator in Action 

Usage

The CodeDom Calculator can be used in one of two ways:  a)  just enter some math expression you want to evaluate using C# Syntax.  b) write a block of code in C# to evaluate something more complex. The first method (method a)  only requires you to type in the math expression as shown in figure 2.

Figure 2 - Evaluating a long function in the CodeDom Calculator

In method b, we do something a bit different. At the top line you place the word answer terminated with a semicolon. After that you write any C# code you wish.  At the end of your code fragment, remember to assign the final answer to the variable answer. You may still leave off the Math class prefix when writing this code.  Figure 3 is an example of summing numbers from 1 to 10 using C# in CodDom.

Figure 3 - Summing numbers from 1 to 10 using code Dom

Creating and Running the Calculator Class

The three steps to evaluating the expression are:  1) Create  C# code  around the function using CodeDom 2) compile the code into an assembly using the CodeDom Compiler 3) Create an instance of the Calculator class 4) Call the Calculate method on the Calculator Class to obtain the answer. Figure 2 shows the CodeDom class we wish to generate. The Calculate method will contain the expression we typed into our CodeDom calculator

Figure 4 - Calculator class in UML Reverse Engineered using WithClass

The assembly that is actually generated by CodeDom for figure 3 is shown in the listing below. We will talk more about how we generated this class with all the cool methods in CodeDom in the next section, but as you can see, our evaluation code was just slapped right into the Calculate method. The reason we place answer; at the top line is so we can just force a dummy line at the top of the Calculate method for large blocks of code (the dummy line being Answer = answer;)  If we had just put in a simple evaluation expression, such as 1 + 1, this same line becomes a Answer = 1 + 1; inside our code.

Listing 1 - CodeDom generated code for the Calculator

 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;
        }
    }
}

The Code

Upon clicking the Calculate button, the code is generated, compiled and run. Listing 2 shows the calculate event handler that executes all of these steps in sequence. Although the details aren't shown here, all of the steps are contained in the methods: BuildClass, CompileAssembly, and RunCode.

Listing 2 - Event Handler for calculating the Math Expression

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

So what does a CodeDom generation look like?  If you look at the classes  in CodeDom carefully, they almost look like a language grammar break down.  Each constructor uses another CodeDom object to construct it and builds a composite of grammar snippets. Table 1 shows the classes we use in this project to construct our assembly and their individual purpose.

CodeDom Object Purpose
CSharpCodeProvider Provider for generating C# Code
CodeNamespace Class for constructing namespace generation
CodeNamespaceImport Generates using statements
CodeTypeDeclaration Generates class structure
CodeConstructor Generates constructor
CodeTypeReference Generates reference for a type
CodeCommentStatement Generates a C# Comment
CodeAssignStatement Generates assignment statement
CodeFieldReferenceExpression Generates a field reference
CodeThisReferenceExpression Generates a this pointer
CodeSnippetExpression Generates any literal string you specify into the code (used to place our evaluation string)
CodeMemberMethod Generates a new method

Table 1 - CodeDom classes used to build the Calculator

Let's look at our CodeDom method for generating code shown in listing 3. As you can see its easier to get your head around code generation with CodeDom, because it breaks down the generation into simple pieces. First we create the generator and in this case we are generating C#, so we create a C# Generator. Then we begin to create and assemble the pieces. First we create the namespace, then we add to it the different import libraries we want to include. Next we create the class. We add to the class a constructor, a property and a method.  In the method we add statements for the method. Inside these statements, we stick the expression that we typed into the text box to evaluate. The expression we typed in is used in the CodeSnippetExpression constructor so we can generate the code directly from our evaluation string. The expression also uses the constructor of the CodeAssignStatement so we can assign it to the Answer property. When we are finished assembling the composite pieces of the CodeDom hierarchy, we just call GenerateCodeFromNamespace with  the CodeDom generator on our assembled namespace. This gets streamed out to our StringWriter and assigned internally to a StringBuilder class where we can extract the whole assembly code from a string.

Listing 3 - Building the Calculator class using CodeDom classes

              /// <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();
            }

Compiling

Compiling is broken down into 3 pieces: Creating the CodeDom compiler, creating the compile parameters, and compiling the code into the assembly as shown in listing 4.

Listing 4  - Compiling the Assembly with 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;
        }

The CreateCompiler code simply constructs a C# CodeDom provider object and creates a compiler object from it.

Listing 5 - Creating the C# Compiler Object

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

We also need to put together compiler parameters as shown in listing 6. Since we are generating an in-memory dll class library, we need to set the appropriate compiler options to make this happen. Also we can use the parameters to add any reference libraries we want to bring into the picture (namely the System library which contains the System.Math class).

Listing 6 - Creating parameters for the compiler

         /// <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;
        }

Finally we need to compile are code. This is accomplished with the method  CompileAssemblyFromSource as shown in listing 7. This method takes the parameters set in listing 5 and the source code of the assembly as a string and compiles the code into an assembly. A reference to the assembly will be assigned in the Compiler results. If there are any errors in the compilation, we write them out to the bottom text box, and set the compiler results to null so we know not to try and run the assembly.

Listing 7 - Compiling the Generated Code into an Assembly using the Compiler Parameters

        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;
        }

Running the Code

Assuming there are no errors after compiling, we can run the assembly. Running the assembly is not accomplished through CodeDom, but rather through Reflection. Reflection allows us to create a Calculator object from our newly created in-memory assembly and run the Calculate method contained within. Listing 8 shows how we run the Calculate method. First we get a reference to the executingAssembly from our results. Then we call CreateInstance on the newly created Calculator class to construct an instance of the class. From our assembly we loop through each class contained within the assembly (in this case only one class, Calculator) and get the class definition. Then we loop through each member in the class and look for the Calculate method.  Once we've obtained the Calculate method, we simply call Invoke on the Calculate method through the object created in CreateInstance. This will execute Calculate on our CodeDom generated assembly and return a resulting double value. The answer is then placed in the result text box.

Listing 8 - Running the Calculate method of the generated Assembly through Reflection

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

Making the Input Compileable

Once of the nice things about accepting code dynamically while the program is running, is that you can decide what is acceptable input before compiling, and then tweak the code input so that it will compile. Another words, the input doesn't initially have to be C#, it just needs to be C# at the point we need to compile it. We decided that the CodeDom calculator would be easier to use if the person typing in the expression didn't have to type the prefix Math before every math function in the System.Math library. By pre-parsing the entered evaluation string, and inserting the Math prefix anywhere it is needed (before compiling), We can make the slightly easier evaluation expression compileable. Also, we felt that the user shouldn't have to worry about case when using the math library, so we handle this situation as well. First we use reflection to create a map of all members of the Math library. Then we use regular expressions to match all words contained in our evaluation strings and see if any of these map to members of the math library. If they do, we append the Math prefix to them before we compile. Listing 9 shows how we can create a map of all members of the Math class through reflection.

Listing 9 - Gathering Members of the Math class through reflection

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

Listing 10 shows how we use the map of the Math members to determine where we should place the Math prefix in our code to make it compileable. By using the regular expression class, we can find all the alphabetic words in our evaluation expression. We can then check all those words against the math member map. Any word that is the same name as  a System.Math member will be replaced with the Math prefix and the member name contained in the map. Because the math member map key is upper case and the value in the math member is the proper case, the person typing the evaluation string need not worry about case when typing. Another words, no matter what case the user types in, the evaluation words will be replaced with the proper case previously read in through Reflection of the System.Math library.

Listing 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;
        }

Conclusion

CodeDom opens up a world of possible dynamic coding that we can conjure on the fly. Code that writes itself may not always bring to mind stories only found in science fiction. Also there are other practical uses of dynamic code generation such as Aspect Oriented Programming, dynamic state machines, and powerful script engines. I think we will see many more uses for this potent technique. In the meantime, keep yours eyes open for the next generation of the .NET framework while coding in C#.

阅读(4751) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册