CodeDom计算器——动态计算数学表达式的实现
序言:前几天整理资料时发现以前翻译的一篇关于CodeDom的文章,虽然题材比较老了,但还是可能对部分兄弟有用,贴出来与大家共享,不妥之处敬请指出。原文来自CSharpCorner:http://www.c-sharpcorner.com/UploadFile/mgold/CodeDomCalculator08082005003253AM/CodeDomCalculator.aspx
介绍:
借助CodeDom和Reflection我们可以动态编译C#代码并使之在程序中任何地方都能运行。这个强大的特性允许我们创建在Windows窗体甚至C#代码行中都可以运算数学表达式的CodeDom计算器。首先我们需要借助System.Math类来进行计算,但我们并不需要在算式前加上Math. 前缀,下面将向您展现CodeDom计算器是如何实现的。
图 1 – 运行中的CodeDom计算器
用法:
CodeDom可依下列两种方法使用:
1、用C#语法输入你要计算的数学表达式
2、写一个计算复杂算式的C#代码块
第一种方法仅需要按图2中的方法输入算式即可。
图 2 – 在CodeDom计算器中运算一个较长的算式
在第二种方法中我们实现的手段略有差异。最上面一行写一个answer加上分号,之后你就可以写任何C#代码,在代码片断的末端将你需要得到的答案赋予answer变量。在写代码时同上也不用加Math类前缀,图3是用CodeDom求从1到10的和的示例:
图 3 – 用CodeDom计算从1到10的和
创建并运行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;)。如果我们输入一个简单的表达式如1+1,在代码内将产生一行Answer = 1 + 1;
列表 1 – CodeDom为计算器产生的代码
namespace ExpressionEvaluator { public class Calculator /// Default Constructor for class // The Answer property is the returned result set /// Calculate an expression } |
代码分析
点击计算按钮后, 代码产生、编译、运行。 列表2 展示了按顺序执行这几步的calculate event handler。尽管这不是全部的代码,所有的步骤已经在BuildClass, CompileAssembly和RunCode方法里全部包括:.
列表 2 – 计算数学表达式的Event Handler
private void btnCalculate_Click(object sender, System.EventArgs e) // change evaluation string to pick up Math class members // build the class using codedom // compile the class into an in-memory assembly. // write out the source code for debugging purposes // if the code compiled okay, |
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> StringWriter sw = new StringWriter(_source); myNamespace.Imports.Add(new CodeNamespaceImport("System")); //Build the class declaration and member variables classDeclaration.IsClass = true; //default constructor //home brewed method that uses CodeDom to make a property //Our Calculate Method myMethod.Name = "Calculate"; // Include the generation below if you want your answer to pop up in a message box // return answer classDeclaration.Members.Add(myMethod); //write code |
编译
编译被分解为3个部分: 创建CodeDom编译器,创建编译参数,并如列表4所示将代码编译进程序集。
列表 4 - 使用CodeDom编译程序集
/// <summary> private CompilerResults CompileAssembly() return results; |
CreateCompiler代码创建C# CodeDom provider对象并从其中创建一个编译器对象。
列表 5 – 创建C#编译器对象
ICodeCompiler CreateCompiler() |
如列表6所示,我们需要将编译器参数放到一起。我们还需要设定适当的编译器选项在内存中生成dll类库,我们也可以使用这些参数来添加各种引用库到包含System.Math类的系统库中。
列表 6 – 为编译器创建参数
/// <summary> /// Creawte parameters for compiling /// </summary> /// <returns></returns> CompilerParameters CreateCompilerParameters() return compilerParams; |
最终我们需要编译的是代码。这是用CompileAssemblyFromSource方法完成的,如列表7所示。这个方法提取列表5设定的参数与字符串形式的代码集合并将代码编译为一个程序集。对该程序集的引用将指派给编译器结果。如果编译过程中有错误,我们在地步的文本框中输出并设定编译器结果为null,这样就不用再去尝试编译并运行程序集了。
列表 7 – 使用编译器参数编译创建的代码生成程序集
private CompilerResults CompileCode(ICodeCompiler compiler, CompilerParameters parms, string source) CompilerResults results = compiler.CompileAssemblyFromSource( //Do we have any compiler errors? 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) try //can't call the entry method if the assembly is null object assemblyInstance = executingAssembly.CreateInstance("ExpressionEvaluator.Calculator"); //Use reflection to call the static Main function Module[] modules = executingAssembly.GetModules(false); //loop through each class and each method that was defined foreach (Type type in types) // place the answer on the win form in the result text box } } } } |
使输入的内容可编译
能够在编译之前决定输入的什么是可编译的并筛选可编译的部分是一件程序运行时动态接受代码的乐事。同理,输入的代码不一定要是C#的,我们只需要在编译前得到C#代码即可,我们决定将CodeDom计算器做的更简单易用,当一个人输入表达式时不一定要输入System.Math 类库的Math前缀。通过对输入字符串的预处理,在需要的地方自动插入Math前缀(当前这些工作应该在编译前完成),我们可以提高输入表达式的可编译性。同时,用户不用担心Math类库的使用场合,我们也可以预处理类似情形。首先使用反射来创建一个Math类库所有成员的图,然后使用正则表达式匹配释放字符串得所有单词并观察其是否是Math类库图中的成员。列表9向大家展示了我们如何通过反射创建一个包含所有Math类成员的图。
列表 9 – 使用Reflection收集到的Math类的成员
ArrayList _mathMembers = new ArrayList(); void GetMathMemberNames() try //Use reflection to get a reference to the Math class Module[] modules = systemAssembly.GetModules(false); //loop through each class that was defined and look for the first occurrance of the Math class // get all of the members of the math class and map them to the same member MemberInfo[] mis = type.GetMembers(); foreach (MemberInfo mi in mis) } } } catch (Exception 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> // look for regular expressions with only letters // track all functions and constants in the evaluation expression we already replaced // find all alpha words inside the evaluation function that are possible functions foreach (Match m in matches) // we matched it already, so don't allow us to replace it again // return the modified evaluation string |
结论
CodeDom展现给我们一个动态编码成为可能的魔法世界,从此代码编写代码将不再是仅能在科幻小说中看到的故事。除此之外还有其他动态代码生成的实用方法如AOP、动态状态机和强大的脚本引擎。我们将见到这种富有潜力的技术得到更多应用,同时,在进行C#编码时请时刻关注下一代.NET Framework。