如何在.NET中实现eval函数
—源代码的动态编译和执行
撰文/阎宏
(用户名jeffyan77地址javapatterns@hotmail)
目录
摘要 (1)
引言 (1)
需求和用户界面 (2)
对象化的代码生成 (2)
动态的代码编译 (3)
eval函数 (5)
用户脚本代码的编译和执行 (6)
小节 (7)
附录 (8)
参考文献 (10)
摘要
本文讨论了在.NET框架中动态地编译源代码和执行源代码的技术,并给出了两个简单的例子,以
及它们在的VB.NET和C#.NET语言中的源代码。
引言
writeline函数曾经有很多朋友问过我这样一个问题:怎样动态地执行一段运行时间输入的代码?比如用户可以在系统运行的时候,以字符串方式输入一段计算式:1+2*4+4*5-6,程序得到上面这个计算式之后,将计算式的结果算出。使用过dBase,Foxpro和Clipper的读者可能会联想起eval这个函数(有时叫做evaluator)。这些以解释方式运行的语言,允许系统以字符串方式传入一个计算式,这个eval 函数会
把计算结果返回。计算式里面含有什么内容,可以由程序在运行时间动态地直接或者间接地根据用户所输入的数据决定。在应用程序编译的时候,这个计算式的内容还没有确定,因此也不可能被编译。编译或者解释编译必然是在程序运行时间完成的。
更为高级的做法,是允许一个系统的用户编写自己的脚本代码,由系统在运行时间进行编译、执行。一些高级应用系统,比如Crystal Reports(水晶报表)就允许用户自行定义一些函数,由系统将之存储起来,并在合适的时间执行这些函数。这些函数在水晶报表系统被编译的时候,尚不存在,它们是动态地被编译的。
很遗憾,Visual Basic 6.0以及以前的版本并不提供类似于eval函数这样的功能;而除非使用某种ActiveScripting控件,使用Visual Basic写成的系统往往也不能提供用户定义的脚本功能。
令人高兴的是,.NET框架提供了非常强大的代码编译、动态的类加载功能,这些功能只要善加利用,不仅仅可以提供简单得eval函数的功能,而且完全可以提供用户脚本功能。本文拟就简单的eval函数的实现问题作一详尽的讨论。对实现用户脚本功能的读者来说,本文仅仅是一个开始。
需求和用户界面
首先需要更为精确地定义出需要完成的任务:
第一、用户可以在运行时间给出一个计算式(比如1+2*4+4*5-6)
第二、这个计算式将以字符串的方式传入到系统中
第三、系统计算出这个计算式的结果
第四、系统将计算结果返还给用户
第五、如果系统出现异常,应当将异常的内容显示出来
根据上面的需求,可以设计一个简易的用户界面如下。可以看出,这个用户界面的左上方是一个可供用户输入计算式的文字框,在用户点击“Eval”按键之后,系统将计算式的计算结果显示在“=”后面,并将可能的出错信息显示在下面的Label
对象化的代码生成
VB.NET和C#.NET均是纯粹的面向对象的编程语言,在这样的语言中,代码都是以类为单位的。换言之,能够进行动态编译的不可能是一个计算式或者函数,而必须是一个类。为了把这个计算式封装到一个类里面,本文采取下面的一个简单的类:
代码清单1、对计算式进行封装后得到的MyNewClass类
Imports System
NameSpace MyNamespace
Public Class MyNewClass
Public Shared Function MySub()
Return 1+2*4+4*5-6
End Function
End Class
End NameSpace
注意其中使用了一个示意性的计算式“1+2*4+4*5-6”,这个计算式必须替换成为用户自己输入的计算式。
换句话说,当用户输入一个计算式的时候,本系统会将这个计算式封装到上面这个类中,然后将这个类送到编译器进行编译。假设这个封装计算式的功能都在一个叫做EvalClassFactory的类中实现,那么这个类中需要如下的Create方法对输入的计算式进行封装:
代码清单2、对计算式进行封装的EvalClassFactory类
Public Class EvalClassFactory : Implements IClassFactory
Public Function Create(ByVal formula As String) As String _ Implements IClassFactory.Create
Dim ret As New StringBuilder("")
ret.Append("Imports System").Append(vbCrLf)
ret.Append("NameSpace MyNamespace").Append(vbCrLf)
ret.Append("Public Class MyNewClass").Append(vbCrLf)
ret.Append("    Public Shared Function MySub()").Append(vbCrLf)        ret.Append("        Return ").Append(formula).Append(vbCrLf)
ret.Append("    End Function").Append(vbCrLf)
ret.Append("End Class").Append(vbCrLf)
ret.Append("End NameSpace")
Debug.WriteLine(ret.ToString)
Return ret.ToString
End Function
End Class
其中使用了一个接口作为抽象类型,因为本文在后面还会需要一个实现这个接口的另一个具体类ScriptClassFactory提供一个简单的用户脚本的编译和讨论。
我们把EvalClassFactory类称为代码工厂类。熟悉设计模式的读者里可以可以看出,这里使用了工厂方法模式,参见下图:
动态的代码编译
下面来研究本文的核心部分,也就是代码如何动态地编译。这部分功能封装在Evaluator类中。
首先系统需要创建一个VB.NET编译器的实例
代码清单3.1
Dim compiler As ICodeCompiler
Dim compilerParams As CompilerParameters
compiler = New VBCodeProvider().CreateCompiler()
注意如果被动态编译的语言不是VB.NET而是C#,那么就应当将上面的VBCodeProvider改为CSharpCodeProvider。
然后加载编译所需要的库集(Assembly)。一般来说单单为了eval函数,我们并不需要Forms库集;但是后面讨论用户脚本代码编译的时候,为了支持操纵Windows控件,就需要
System.Windows.Forms库集。
代码清单3.2
compilerParams = New CompilerParameters()
compilerParams.ReferencedAssemblies.Add("System.dll")
compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll")
然后将库集加载到内存空间:
代码清单3.3
compilerParams.GenerateInMemory = True
假设这个时候我们已经得到了完整的封装类代码code,现在就可以将它传递给编译器进行编译:
代码清单3.4
Dim compiled As CompilerResults
compiled = compiler.CompileAssemblyFromSource(compilerParams, code)
如果编译失败的话,就应当终止执行,并记录下所有的错误信息,以便提供给用户:
代码清单3.5
If (Not compiled.Errors.HasErrors) Then
errorMsg = "No error."
Else
'Create Error String
errorMsg = compiled.Errors.Count.ToString() + " error(s):"
Dim iCount As Integer
For iCount = 0 To compiled.Errors.Count - 1
errorMsg = errorMsg & vbCrLf & "Line: " _
& compiled.Errors(iCount).Line.ToString & " - " _
& compiled.Errors(iCount).ErrorText
Return Nothing
Next iCount
End If
假设编译是成功的,现在就应当加载封装类了,记住我们的封装类位于MyNamespace名空间,类名是.MyNewClass:
代码清单3.6
Dim myAssembly As System.Reflection.Assembly
myAssembly = compiled.CompiledAssembly
Dim loadedObject As Object
loadedObject = myAssembly.CreateInstance("MyNamespace.MyNewClass ")
系统必须检验加载是否成功,如果不成功,就应当终止执行:
代码清单3.7
If (loadedObject Is Nothing) Then
MessageBox.Show("Couldn't load class.")
Return Nothing
End If
最后如果加载是成功的,就应当调用MySub()方法,并将结果返还给调用端。但是因为这个类是动态加载的,所以只能通过反身映射(Reflection)调用:
代码清单3.8
Try
Dim retValue As Object = loadedObject.GetType().InvokeMember( "MySub", BindingFlags.InvokeMethod, Nothing,
loadedObject, Nothing)
Return retValue
Catch e As Exception
MessageBox.Show(e.Message, "Compiler Demo",
MessageBoxButtons.OK, MessageBoxIcon.Information)
End Try
本小节所谈论的代码不仅仅适用于eval函数,也同样适用于用户脚本功能,因为两者的区别只限于MySub()方法的内容不同。
为了在两种情况下都能使用本小节的代码,我们将这些代码放到一个Private方法runcode 中,作为参数接受封装类的代码:
代码清单3.9
Private Function runcode(ByVal code As String)
eval函数
实现这个eval函数功能的就是Evaluator.eval()方法:
代码清单4、Evaluator.eval()函数
Public Function eval(ByVal code As String) As Object
factory = New EvalClassFactory()
Return runcode(factory.Create(code))
End Function
这个方法调用代码生成工厂EvalClassFactory生成封装了计算式的封装类,然后调用私有方法runcode()执行这个封装类的MySub()方法。
下面是系统在运行时的情况:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。