可扩展的表达式求值包 (EEEP!)






3.86/5 (17投票s)
2004年4月26日
6分钟阅读

84761

952
提供对数学和基于字符串的表达式的运行时求值。
引言
运行时编译和执行是一个理想的语言特性;它是“小语言”概念的一部分,常被用作系统和应用程序的灵活控制方法。然而,更基本和普遍的需求是对简单的数学和字符串表达式进行求值。
许多脚本语言,包括 Perl、JScript/JavaScript、VBScript 和 Transact-SQL,都包含一个函数(通常称为 "eval" 或 "evaluate",在 T-SQL 中是 EXEC(UTE)
)来计算并返回一个字符串表达式的结果。(这些表达式有时也可以作为普通代码行执行。例如,在 JavaScript 中,你可以动态创建代码,这些代码可以即时解释以影响页面元素;然而,这超出了本文的范围。)
这在 .NET 和其他现代编程平台(如 Java 2)中是一个普遍被要求的功能,这并不奇怪。Java 程序员为了达到这个效果,通常被迫求助于对动态生成的代码进行命令行编译,这对性能不利。.NET 赋予了我们通过 ADO.NET 动态求值表达式的能力,并且还通过 CodeDom
命名空间提供了一个方便的内存中编译功能。
关于项目
我有时仅为了利用 SQL Server 和其他数据库的表达式求值能力而使用它们,但这并不是数据库服务器的最佳用途。此外,Transact-SQL 对于简单的数学和文本处理任务来说是没问题的,但 JScript 内置的功能要丰富得多。我最初的设计目标是为 Transact-SQL、JScript.NET 和 VBScript 中的表达式提供可重用的求值能力,但最终放弃了对 VBScript 实现的尝试。
我创建了一套可扩展的类,用于封装 .NET 中的两种主要动态求值能力。这个设计主要围绕 IEvaluator
接口展开;该接口由基类 Evaluator
部分实现,而 Evaluator
类又被 ADOEvaluator
和 JScriptEvaluator
类扩展。我的主要目标是创建有用的代码,即使是初级程序员也能轻松使用;我也第一次涉足了 .NET 反射,并在此过程中了解了一些 ADO 的特性。
所有代码,连同一个 VS .NET 2003 项目文件和一个通过使用 NDoc 生成的帮助文件,都包含在本页顶部可下载的 .zip 文件中。为了将代码导入你自己的项目,你需要引用 System.Data
来使用 ADOEvaluator
,以及引用 Microsoft.JScript
来使用 JScriptEvaluator
。
这个项目的主要创新在于为无限的表达式求值实现提供了一个简化的接口,同时支持在 .NET API 中流行的 {0} {1} 风格的参数传递方式。它也作为外观模式的一个基本示例。
API
超类 Evaluator
包含两个重要的方法:IsValid()
,用于测试表达式的有效性;以及 Evaluate
,用于计算表达式并返回值。这两个方法都被重载,以允许多达十个可替换的参数,此外还有一个只接受表达式本身的版本。如果选择了带额外参数的方法,参数替换的执行方式类似于 String.Format
,第一个参数值替换传入表达式中的 {0}
,第二个替换 {1}
,依此类推。如果需要,可以通过 IEvaluator
的 ArgumentPrefix
和 ArgumentSuffix
属性更改这些索引值的前缀和后缀。
IsSynchronized
属性可用于控制每个 Evaluator
子类的锁定行为。默认情况下,它被设置为 false
,但在超类中提供了同步支持,以便于在多线程环境中安全地包装和使用非线程安全的代码。如果一个新的实现使用了 Evaluator
基类,唯一需要(或可)实现的抽象方法是 EvaluateImpl()
方法,它接受一个字符串参数。
非常简单的 ADOEvaluator
类包装了一个 DataTable
,以便调用该表的 Compute
方法。我用 ildasm.exe 检查了这个类,发现其底层实际上是实例化和求值一个 System.Data.DataExpression
对象;不幸的是,这个类是 internal
的,因此不能直接使用。这看起来很奇怪,但大概是 .NET 的设计者们认为它与数据库访问没有直接关系,只会让 ADO.NET API 变得混乱。
我最初写过一个版本的 JScriptEvaluator
类,它在静态初始化期间即时编译一个 JScript.NET 类,其唯一目的是将 JScript.NET 的 eval
函数公开为一个类方法。它每次都使用一个随机的类名,以确保即使是刻意为之,也几乎不可能与另一个类发生命名冲突。不过,那是在我发现 Microsoft.JScript.Eval.JScriptEvaluate()
方法之前。去除了通过反射调用生成类的方法的需要,极大地提升了代码的性能。
示例代码
代码不言自明。你创建一个所需类型的 IEvaluator
实例,可以是自己的实现,也可以是 ADOEvaluator
或 JScriptEvaluator
。接下来,调用 Evaluate
,并将返回的对象转换为预期的返回类型。出于性能原因,我更喜欢使用 JScript.NET 而不是 ADO.NET,你可能也会这样做。
示例 1:一个简单的数学表达式
using Expressions;
// ...
IEvaluator evaluator = new JScriptEvaluator();
Console.WriteLine(evaluator.Evaluate("35 * (100 + 9)"));
输出
3815
示例 2:替换值,类似于 String.Format()
这里的占位符 {0}
、{1}
和 {2}
被可选参数替换。这个例子也展示了不带任何可选参数时 IsValid()
方法的用法。
using Expressions;
// ...
IEvaluator evaluator = new JScriptEvaluator();
Console.WriteLine(evaluator.Evaluate("{0} * ({1} + {2})", 35, 100, 9));
Console.WriteLine(evaluator.IsValid("bling bling"));
输出
3815
False
测试应用程序
测试应用程序的源代码位于单个文件 Test.cs 中。这个命令行应用程序允许你选择使用两个默认实现中的哪一个来求值一个语句;它还允许包含一个简单的性能测试,让你大致了解一个特定语句在 ADO.NET 或 JScript.NET 下的求值时间。下面是该应用程序的一个示例输出。
请注意,在我的 P4 笔记本电脑上进行的这个非正式测试中,对于两个测试中使用的简单数学表达式,JScript.NET 求值语句的速度是 ADO.NET 的七倍多。还要注意的是,对于这样一个简单的计算,20 微秒的速度并不算快,这强烈表明如果性能是关键问题,就不应过度使用这种技术。测试方法很简单:表达式在一个紧凑循环中求值 1,000 次,然后将经过的纳秒数除以 1,000,得出单次求值的大致时间。
##########################################################
# #
# EXPRESSION EVALUATION TEST APPLICATION, v1.0 #
# #
# No warranties, either express or implied, #
# are made as to the fitness of this #
# application for the purpose of turning #
# aside rampaging hordes of wombats. This is #
# no laughing matter... stop laughing. #
# #
##########################################################
(E)valuate or (Q)uit? e
Evaluation type: (A)DO or (J)Script? a
Performance test: (Y)es or (N)o? y
Expression: (1.789 * 154) / .32
RESULTS
-------
Expression : (1.789 * 154) / .32
Nanoseconds per evaluation : 150216
Evaluation result : 860.95625
(E)valuate or (Q)uit? e
Evaluation type: (A)DO or (J)Script? j
Performance test: (Y)es or (N)o? y
Expression: (1.789 * 154) / .32
RESULTS
-------
Expression : (1.789 * 154) / .32
Nanoseconds per evaluation : 20001.38
Evaluation result : 860.95625
(E)valuate or (Q)uit? q
致谢
感谢 Yaron K. 首次让我注意到 DataTable.Compute()
方法,也感谢 Heath Stewart 提出了他一贯有用的见解。