表达式求值Spring.Expressions
简介
Spring.Expressions命名空间可以用一种强大的表达式语言在运行时操作对象。这种语言可以读写属性值、调用方法、访问数组/集合/索引器的元素、进行算术和逻辑运算,同时支持命名变量,并且能够通过名称从IoC容器获取对象。
在Spring.NET中,该命名空间是其它许多功能(比如IoC容器中的增强属性求值、数据验证框架及ASP.NET的数据绑定框架)的基础。如果需要以对象的运行时状态为依据进行求值,您还会发现一些更加有趣的特性。对于有Java背景的开发人员来说,Spring.Expressions命名空间的功能和Java的OGNL(Object Graph Navigation Language)很相似。
本章将用一个Inventor类及与其相关的类型为例来讲解表达式语言。这些类的代码和使用到的数据都列举在本章的最后一节本章所用的示例类型中。这些类型是从Spring.Expression命名空间的NUnit测试项目中拿出来的,您可以参考该项目的源码来学习表达式的其它用法。
表达式求值
Spring.Expressions命名空间的核心类是ExpressionEvaluator,其主要方法如下:
public static object GetValue(object root, string expression); public static object GetValue(object root, string expression, IDictionary variables) public static void SetValue(object root, string expression, object newValue) public static void SetValue(object root, string expression, IDictionary variables, object newValue)
其中,参数root就是我们要在其上进行求值的目标“根”对象,参数expression是用于求值的字符串表达式。其余参数用来指定在表达式中使用的变量,稍后再讨论它们。下面的代码从Inventor实例中读取属性值。Invertor类的代码可以参见本章所用的示范类型。
Inventor tesla = new Inventor("Nikola Tesla", new DateTime(1856, 7, 9), "Serbian"); tesla.PlaceOfBirth.City = "Smiljan"; string evaluatedName = (string) ExpressionEvaluator.GetValue(tesla, "Name"); string evaluatedCity = (string) ExpressionEvaluator.GetValue(tesla, "PlaceOfBirth.City"));
上面的代码执行后,变量evalutedName的值为"Nikola Tesla",evalutedCity的值为"Smijan"。在表示嵌套属性时要用小数点分隔路径。设置属性值与读取属性值的用法类似,如果我们要改写历史,将Tesla的出生地改为其它城市,可以用下面的代码:
ExpressionEvaluator.SetValue(tesla, "PlaceOfBirth.City", "Novi Sad");
Spring.Expressions命名空间中另有一个不太常用到的类:Expression。如果要用某个表达式针对同一对象进行多次求值,该类可以提高性能。ExpressionEvaluator类会在每次调用GetValue或SetValue时解析表达式;而Expression则会将解析和反射的结果缓存起来。该类的部分方法如下(按:方法很多,参见API文档):
public static IExpression Parse(string expression) public override object Get(object context, IDictionary variables) public override void Set(object context, IDictionary variables, object newValue)
可以用下面的代码读取前面例子的属性值:
IExpression exp = Expression.Parse("Name"); string evaluatedName = (string) exp.GetValue(tesla, null);
在使用ExpressionEvaluator的时候,会涉及到几个异常类型。如果引用的属性不存在,会抛出InvalidPropertyException;如果在遍历嵌套属性的路径时遇到了null值,会抛出NullValueInNextedPathException(按:对于嵌套的属性,除了最末一个点后的实际属性,路径当中的值都必须不能为空);如果传值时发生其它错误,会抛出ArgumentException和NotSupportedException异常。
表达式语言有自己的语法,并且使用ANTLR来构建语法分析器和解析器,语法错误在解析时就会被捕获。如果想深入了解解析器的实现细节,可以参考源文件中的语法文件Expression.g。另外,目前Spring.NET使用的ANTLR.DLL程序集是用Spring.NET的key标记的。将来ANTLR会在新版本中使用自己的强命名程序集。
语言参考
文字表达式
文字表达式可以表示字符串、日期、数字值(int、real和hex)、布尔及空值。字符串以单引号包围。表达式内的单引号要用反斜线进行转义。下面是文字表达式的一些简单用法。注意文字表达式一般不会这样单独使用,多是作为复杂表达式的一部分,比如作为逻辑比较操作符的一个操作数。
string helloWorld = (string) ExpressionEvaluator.GetValue(null, "'Hello World'"); // evals to "Hello World" string tonyPizza = (string) ExpressionEvaluator.GetValue(null, "'Tony\\'s Pizza'"); // evals to "Tony's Pizza" double avogadrosNumber = (double) ExpressionEvaluator.GetValue(null, "6.0221415E+23"); int maxValue = (int) ExpressionEvaluator.GetValue(null, "0x7FFFFFFF"); // evals to 2147483647 DateTime birthday = (DateTime) ExpressionEvaluator.GetValue(null, "date('1974/08/24')"); DateTime exactBirthday = (DateTime) ExpressionEvaluator.GetValue(null, " date('19740824T131030', 'yyyyMMddTHHmmss')"); bool trueValue = (bool) ExpressionEvaluator.GetValue(null, "true"); object nullValue = ExpressionEvaluator.GetValue(null, "null");
注意‘Tony\\'s Pizza’里多出来的那个反斜线是为了遵循C#的语法(按:在字符串中\\转义为\)。数字中可以出现负号、指数幂和小数点。默认情况下,实数由Double.Parse来解析,除非指定了格式字符“M”或“F”。对于包含格式字符“M”的数字,将由Decimal.Parse解析;包含“F”的数字则由Single.Parse解析。如果在日期文本中指定了两个参数(如上面例子倒数第三句),就会用DateTime.ParseExact进行解析。注意在解析时所有类型的Parse方法都会在内部引用CultureInfo.InvariantCulture作为当前的语言文化信息。
属性,数组,列表,字典,索引器
前面表达式求值一节中曾经讲到,用属性路径表示嵌套的属性是很简单的,只需要用小数点分隔嵌套的属性名即可。例子中Invertor的两个实例:pupin和tesla,已经用本章用到的示例类型中定义的数据赋过值了。如果要继续“向下”访问Tesla生日中的年份和Pupin出生地中的城市信息,可以用下面的代码:
int year = (int) ExpressionEvaluator.GetValue(tesla, "DOB.Year")); // 1856 string city = (string) ExpressionEvaluator.GetValue(pupin, "PlaCeOfBirTh.CiTy"); // "Idvor"
眼尖的人可能已经发现了,pupin的出生地大小写交替,显得乱七八糟。这并非排版错误,而是有意为之,为的是证明表达式语言是忽略大小写的。
在表达式中,可以用方括号获取数组和列表的元素:
// Inventions Array string invention = (string) ExpressionEvaluator.GetValue(tesla, "Inventions[3]"); // "Induction motor" // Members List string name = (string) ExpressionEvaluator.GetValue(ieee, "Members[0].Name"); // "Nikola Tesla" // List and Array navigation string invention = (string) ExpressionEvaluator.GetValue(ieee, "Members[0].Inventions[6]") // "Wireless communication"
在访问字典的元素时,键值要用单引号括起来。在本例中,因为字典Officers的键值是字符串类型,我们可以直接使用文本字符串:
// Officer's Dictionary Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers['president']"; string city = (string) ExpressionEvaluator.GetValue(ieee, "Officers['president'].PlaceOfBirth.City"); // "Idvor" ExpressionEvaluator.SetValue(ieee, "Officers['advisors'][0].PlaceOfBirth.Country", "Croatia");
也可以在方括号中直接使用其它表达式,例如变量名或某个类型的静态属性/方法名。我们会在其它小节中讨论这些内容。
同样的,索引器也用方括号访问。下面是一个例子。也可以访问多维索引器。
public class Bar { private int[] numbers = new int[] {1, 2, 3}; public int this[int index] { get { return numbers[index];} set { numbers[index] = value; } } } Bar b = new Bar(); int val = (int) ExpressionEvaluator.GetValue(bar, "[1]") // evaluated to 2 ExpressionEvaluator.SetValue(bar, "[1]", 3); // set value to 3
定义内联的数组、列表和字典
除了能在表达式中引用容器中的数组、列表和词典外,Spring.NET还允许在表达式中定义内联数组、列表和词典。内联的列表用花括号定义,在花括号内,用逗号来分隔列表的元素。
{1, 2, 3, 4, 5} {'abc', 'xyz'}
如果需要初始化一个强类型的数组,而非弱类型的列表,可以使用数组的初始化操作符:
new int[] {1, 2, 3, 4, 5} new string[] {'abc', 'xyz'}
内联字典的定义表达式有点复杂:首先需要在花括号前用#作为前缀,在花括号内,用逗号将键值对分隔开,键和值之间则用冒号隔开:
#{'key1' : 'Value 1', 'today' : DateTime.Today} #{1 : 'January', 2 : 'February', 3 : 'March', ...}
用这种方式创建的数组、列表和字典可以在任意表达式中使用,稍后我们会在例子中看到。
请注意,虽然前面都是用简单的文字值来定义数组/列表的项和词典的键、值,但这只是一个简单的例子——实际上我们可以用任何有效的表达式来定义元素。
方法
在文字表达式中,可以用标准的c#语法调用方法:
//string literal char[] chars = (char[]) ExpressionEvaluator.GetValue(null, "'test'.ToCharArray(1, 2)")) // 't','e' //date literal int year = (int) ExpressionEvaluator.GetValue(null, "date('1974/08/24').AddYears(31).Year") // 2005 // object usage, calculate age of tesla navigating from the IEEE society. ExpressionEvaluator.GetValue(ieee, "Members[0].GetAge(date('2005-01-01')") // 149 (eww..a big anniversary is coming up ;)
操作符
关系操作符
关系操作符:相等、不等、小于、小于等于、大于、大于等于在文字表达式里都用普通的符号表示。如果被比较的对象实现了IComparable接口,这些操作符就能起作用。另外,文字表达式也支持枚举类型的关系操作,但如果要使用的枚举不是mscorlib中的类型,则需要进行注册,可参见类型注册。
ExpressionEvaluator.GetValue(null, "2 == 2") // true ExpressionEvaluator.GetValue(null, "date('1974-08-24') != DateTime.Today") // true ExpressionEvaluator.GetValue(null, "2 < -5.0") // false ExpressionEvaluator.GetValue(null, "DateTime.Today <= date('1974-08-24')") // false ExpressionEvaluator.GetValue(null, "'Test' >= 'test'") // true
下面是枚举类型的比较:
FooColor fColor = new FooColor(); ExpressionEvaluator.SetValue(fColor, "Color", KnownColor.Blue); bool trueValue = (bool) ExpressionEvaluator.GetValue(fColor, "Color == KnownColor.Blue"); //true
其中FooColor的定义为:
public class FooColor { private KnownColor knownColor; public KnownColor Color { get { return knownColor;} set { knownColor = value; } } }
除了标准的关系操作符,Spring.NET的表达式语言还支持一些功能很强大的特殊关系操作符,这些操作符是从SQL中“借”来的,比如in、like和between,以及is和matches等等,使用这些操作符可以判断某个对象是否是特定的类型,或者判断某个值是否和一个正则表达式相匹配。
ExpressionEvaluator.GetValue(null, "3 in {1, 2, 3, 4, 5}") // true ExpressionEvaluator.GetValue(null, "'Abc' like '[A-Z]b*'") // true ExpressionEvaluator.GetValue(null, "'Abc' like '?'") // false ExpressionEvaluator.GetValue(null, "1 between {1, 5}") // true ExpressionEvaluator.GetValue(null, "'efg' between {'abc', 'xyz'}") // true ExpressionEvaluator.GetValue(null, "'xyz' is int") // false ExpressionEvaluator.GetValue(null, "{1, 2, 3, 4, 5} is IList") // true ExpressionEvaluator.GetValue(null, "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'")) // false ExpressionEvaluator.GetValue(null, @"'5.00' matches '^-?\d+(\.\d{2})?$'") // true
注意like操作符的匹配字符串使用的是VB的语法,而非SQL语法。(注意:在1.1的最终版中可能改变该行为,所以如果使用了like操作符,可能将来需要做必要的修改。)
逻辑操作符
逻辑操作符包括and,or和not,用法如下所示:
// AND bool falseValue = (bool) ExpressionEvaluator.GetValue(null, "true and false"); //false string expression = @"IsMember('Nikola Tesla') and IsMember('Mihajlo Pupin')"; bool trueValue = (bool) ExpressionEvaluator.GetValue(ieee, expression); //true // OR bool trueValue = (bool) ExpressionEvaluator.GetValue(null, "true or false"); //true string expression = @"IsMember('Nikola Tesla') or IsMember('Albert Einstien')"; bool trueValue = (bool) ExpressionEvaluator.GetValue(ieee, expression); // true // NOT bool falseValue = (bool) ExpressionEvaluator.GetValue(null, "!true"); // AND and NOT string expression = @"IsMember('Nikola Tesla') and !IsMember('Mihajlo Pupin')"; bool falseValue = (bool) ExpressionEvaluator.GetValue(ieee, expression);
算术运算符
算术操作符可用于数字、字符串和日期的操作。其中数字和日期可以做减法。乘法和除法则只适用于数字。同时,也可以支持其它类型的数学操作符,如求模(%)和指数幂(^)。见下例:
// Addition int two = (int)ExpressionEvaluator.GetValue(null, "1 + 1"); // 2 String testString = (String)ExpressionEvaluator.GetValue(null, "'test' + ' ' + 'string'"); //'test string' DateTime dt = (DateTime)ExpressionEvaluator.GetValue(null, "date('1974-08-24') + 5"); // 8/29/1974 // Subtraction int four = (int) ExpressionEvaluator.GetValue(null, "1 - -3"); //4 Decimal dec = (Decimal) ExpressionEvaluator.GetValue(null, "1000.00m - 1e4"); // 9000.00 TimeSpan ts = (TimeSpan) ExpressionEvaluator.GetValue(null, "date('2004-08-14') - date('1974-08-24')"); //10948.00:00:00 // Multiplication int six = (int) ExpressionEvaluator.GetValue(null, "-2 * -3"); // 6 int twentyFour = (int) ExpressionEvaluator.GetValue(null, "2.0 * 3e0 * 4"); // 24 // Division int minusTwo = (int) ExpressionEvaluator.GetValue(null, "6 / -3"); // -2 int one = (int) ExpressionEvaluator.GetValue(null, "8.0 / 4e0 / 2"); // 1 // Modulus int three = (int) ExpressionEvaluator.GetValue(null, "7 % 4"); // 3 int one = (int) ExpressionEvaluator.GetValue(null, "8.0 % 5e0 % 2"); // 1 // Exponent int sixteen = (int) ExpressionEvaluator.GetValue(null, "-2 ^ 4"); // 16 // Operator precedence int minusFortyFive = (int) ExpressionEvaluator.GetValue(null, "1+2-3*8^2/2/2"); // -45
赋值
在表达式中可以用等号为属性赋值。因为ExpressionEvaluator类的SetValue方法可以直接用来赋值,所以用等号赋值一般是用在GetValue方法中。在用表达式列表组合多个操作数时,这种赋值方法就很有用,下一小节会讨论表达式列表,现在先看一个使用赋值表达式的例子:
Inventor inventor = new Inventor(); String aleks = (String) ExpressionEvaluator.GetValue(inventor, "Name = 'Aleksandar Seovic'"); DateTime dt = (DateTime) ExpressionEvaluator.GetValue(inventor, "DOB = date('1974-08-24')"); //Set the vice president of the society Inventor tesla = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers['vp'] = Members[0]");
表达式列表
在花括号中用分号分隔多个表达式,可以使这些表达式同时(按:注意此处不是指并发,而是象编程语言中的语句块一样,“同时”执行)执行。表达式列表的返回值是列表中最后一个表达式的值。请看下面的例子:
//Perform property assignments and then return Name property. String pupin = (String) ExpressionEvaluator.GetValue(ieee.Members, "( [1].PlaceOfBirth.City = 'Beograd'; [1].PlaceOfBirth.Country = 'Serbia'; [1].Name )")); // pupin = "Mihajlo Pupin"
类型
很多情况下,都可以通过名称来引用一个类型:
ExpressionEvaluator.GetValue(null, "1 is int") ExpressionEvaluator.GetValue(null, "DateTime.Today") ExpressionEvaluator.GetValue(null, "new string[] {'abc', 'efg'}")
如果类型是定义在mscorlib.dll中的,或是通过TypeRegistry注册过的,都可以通过名称直接引用。我们会在下一小节讨论类型注册。
对于其它类型,则需要使用特殊的T(typeName)表达式:
Type dateType = (Type) ExpressionEvaluator.GetValue(null, "T(System.DateTime)") Type evalType = (Type) ExpressionEvaluator.GetValue(null, "T(Spring.Expressions.ExpressionEvaluator, Spring.Core)") bool trueValue = (bool) ExpressionEvaluator.GetValue(tesla, "T(System.DateTime) == DOB.GetType()")
注意 | |
---|---|
类型的解析是由Spring.NET的ObjectUtils.ResolveType方法完成的,也就是说,定义在表达式中的类型,其解析方式和定义在配置文件中的类型是完全一样的。 |
类型注册
如果要在表达式中使用自定义类型(按:指没有在mscorlib中定义的类型),需要先用TypeRegistry类进行注册。注册以后即可以用名称来引用该类型,比如在new操作符(按:下一小节)或静态属性的引用表达式中就经常会用名称来引用类型。请看下面的例子:
TypeRegistry.RegisterType("Society", typeof(Society)); Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers[Society.President]");
此外,我们也可以在配置文件中用typeAliases节点注册类型。
构造器
在表达式中,可以用new操作符调用类型的构造器。如果类型不是mscorlib中的标准类型,需要先进行注册。请看下面的例子:
// simple ctor DateTime dt = (DateTime) ExpressionEvaluator.GetValue(null, "new DateTime(1974, 8, 24)"); // Register Inventor type then create new inventor instance within Add method inside an expression list. // Then return the new count of the Members collection. TypeRegistry.RegisterType(typeof(Inventor)); int three = (int) ExpressionEvaluator.GetValue(ieee.Members, "{ Add(new Inventor('Aleksandar Seovic', date('1974-08-24'), 'Serbian')); Count}"));
为方便起见,Spring.NET允许在表达式中定义已命名构造器参数,其作用是在对象被初始化之后对其属性赋值,这与标准.NET特性的创建方式类似。比如,下面的表达式创建了一个Inventor实例,并对其Inventions属性赋值。注意表达式中使用的Inventor构造器只有3个参数,最后一个等式就是已命名参数:
Inventor aleks = (Inventor) ExpressionEvaluator.GetValue(null, "new Inventor('Aleksandar Seovic', date('1974-08-24'), 'Serbian', Inventions = {'SPELL'})");
这里唯一要遵守的规则是:已命名参数必须位于所有构造器参数之后,同.NET特性的创建方式一样。
我们已经两次提到.NET特性的创建方式,现在就来看看如何用Spring.NET的表达式语言为.NET特性创建实例。在创建.NET特性的实例时,我们可以用更为简短和熟悉的方式来代替标准的构造器调用,如下:
WebMethodAttribute webMethod = (WebMethodAttribute) ExpressionEvaluator.GetValue(null, "@[WebMethod(true, CacheDuration = 60, Description = 'My Web Method')]");
可以看出,除了其中的前缀@,创建特性所用的表达式与在C#中应用特性的语法在形式上完全一样。
然尔语法上的轻微差异并不是特性表达式与构造器调用表达式之间的唯一区别。在使用特性表达式时,后台所用的类型解析机制与调用构造器时也稍有不同,此时会同时尝试按指定的类型名称和按指定名称加上"Attribute"后缀两种方式来创建特性实例,这与C#编译器的行为是一样的。
变量
在表达式中用#variableName的格式来表示变量名。变量通过一个字典类型的参数传递给ExpressionEvaluator的GetValue或SetValue方法。(按:此处的变量是指表达式的变量,而非由c#定义在代码中的变量;所有的变量都保存在一个字典中,变量名就是字典项的键值,变量值就是字典项的值。)
public static object GetValue(object root, string expression, IDictionary variables) public static void SetValue(object root, string expression, IDictionary variables, object newValue)
变量名是字典的键值。请看下面的例子:
IDictionary vars = new Hashtable(); vars["newName"] = "Mike Tesla"; ExpressionEvaluator.GetValue(tesla, "Name = #newName", vars));
表达式内部求值的结果也可以用字典来保存。下面的例子将Tesla的名字改回原来的值,并保存在键值为oldName的项中:
ExpressionEvaluator.GetValue(tesla, "{ #oldName = Name; Name = 'Nikola Tesla' }", vars); String oldName = (String)vars["oldName"]; // Mike Tesla在索引器中或map中,也可以使用变量名作为参数:
vars["prez"] = "president"; Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers[#prez]", vars);
'#this'和'#root'变量
在表达式中,有两个特殊的变量:#this和#root。
#this变量可以显式的引用表达式中当前节点的上下文:
// sets the name of the president and returns its instance ExpressionEvaluator.GetValue(ieee, "Officers['president'].( #this.Name = 'Nikola Tesla'; #this )")
而#root变量用来引用表达式的根上下文:(按,注意它们的含义:表达式的“根上下文”,就是要在其上求值的对象,也就是说,是GetValue方法的第一个参数;而“表达式的节点”,是指表达式中以小数点分开的各个部分,比如node1.node2.node3,node3所在的上下文即为node2)
// removes president from the Officers dictionary and returns removed instance ExpressionEvaluator.GetValue(ieee, "Officers['president'].( #root.Officers.Remove('president'); #this )")
三元操作符(If-Then-Else)
可以在表达式中用下面的三元操作符来表示if-then-else条件逻辑:
String aTrueString = (String) ExpressionEvaluator.GetValue(null, "false ? 'trueExp' : 'falseExp'") // trueExp
上面的false值使得表达的返回值为“trueExp”。下面是一个更为真实的例子:
ExpressionEvaluator.SetValue(ieee, "Name", "IEEE"); IDictionary vars = new Hashtable(); vars["queryName"] = "Nikola Tesla"; string expression = @"IsMember(#queryName) ? #queryName + ' is a member of the ' + Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; String queryResultString = (String) ExpressionEvaluator.GetValue(ieee, expression, vars)); // queryResultString = "Nikola Tesla is a member of the IEEE Society"
列表的投影(Projection)和选择(Selection)
列表的投影和选择是表达式语言一项很强大的功能,可以从源列表中选出某些列或某些行来创建一个新列表。也就是说,投影可以看做是和SQL中Select语句功能相似的列选择器,而选择则相当于where子句。
例如,假设我们需要一个发明家的出生地列表,通过投影PlaceOfBirth.City属性,很容易获取此列表:
IList placesOfBirth = (IList) ExpressionEvaluator.GetValue(ieee, "Members.!{PlaceOfBirth.City}") // { 'Smiljan', 'Idvor' }
我们也可以获取社团中要员的名称列表:
IList officersNames = (IList) ExpressionEvaluator.GetValue(ieee, "Officers.Values.!{Name}") // { 'Nikola Tesla', 'Mihajlo Pupin' }
从这两个例子可以看出,投影语句使用的语法是!{projectionExpression},它返回的新列表长度和原列表一样,但元素的类型一般不相同。
另一方面,选择语句的语法为?{selectionExpression},它返回的新列表是从源列表中过滤出的一个子集。例如,通过选择语句我们很容易获取一个塞尔维亚籍发明家的列表:
IList serbianInventors = (IList) ExpressionEvaluator.GetValue(ieee, "Members.?{Nationality == 'Serbian'}") // { tesla, pupin }
或者过滤出声纳的发明者:
IList sonarInventors = (IList) ExpressionEvaluator.GetValue(ieee, "Members.?{'Sonar' in Inventions}") // { pupin }
或者,同时使用投影和选择来获取声纳发明者的全名:
IList sonarInventorsNames = (IList) ExpressionEvaluator.GetValue(ieee, "Members.?{'Sonar' in Inventions}.!{Name}") // { 'Mihajlo Pupin' }
为方便起见,Spring.NET的表达式语言可以用专门的语法选择第一个和最后一个匹配的列表项。注意这与正则表达式有区别,在用正则表达式选择时,如果找不到匹配项就会返回一个空列表,而首尾项选择表达式则会在找到匹配项时返回该项的引用,找不到时返回null。要返回第一个匹配项,要在选择表达式中用^{代替?{,选择最后一个匹配项时应该用${:
ExpressionEvaluator.GetValue(ieee, "Members.^{Nationality == 'Serbian'}.Name") // 'Nikola Tesla' ExpressionEvaluator.GetValue(ieee, "Members.${Nationality == 'Serbian'}.Name") // 'Mihajlo Pupin'
请注意,在上面代码中我们直接使用选择结果访问了Name属性,因为通过首尾项选择表达式返回的不是列表,而是列表中的元素。
集合处理器和聚合器(Aggregator)
除了列表的投影和选择,Spring.NET表达式语言还支持几种集合处理器,比如distinct、nonNull和sort;以及一系列通用的聚合器,如max、min、count、sum和average。
处理器和聚合器的区别是:处理器会返回一个新的或者转换过的集合,而聚合器返回的则是一个单一值。除此之外它们非常相似——处理器和聚合器都使用标准的方法调用表达式来处理集合的节点,它们都很简单,也很容易组成处理器链。
Count聚合器
通过Count聚合器,可以安全的获取集合元素的总数。Count适用于所有集合类型,包括数组,我们不需要考虑应该使用Count属性还是Length属性来获取集合的大小,并且,即便集合为null,也不会抛出NullReferenceException异常,而是返回0,在复杂的表达式中,这样要比使用.NET的标准属性更为安全。
ExpressionEvaluator.GetValue(null, "{1, 5, -3}.count()") // 3 ExpressionEvaluator.GetValue(null, "count()") // 0
Sum聚合器
如果列表元素是数字值,可以用sum聚合器计算所有元素的总和。如果列表中数字的类型或精度不一致,则会自动执行必要的转型,然后以其中最大精度的类型返回结果值。如果集合中的元素都不是数字,就会抛出InvalidArgumentException异常。
ExpressionEvaluator.GetValue(null, "{1, 5, -3, 10}.sum()") // 13 (int) ExpressionEvaluator.GetValue(null, "{5, 5.8, 12.2, 1}.sum()") // 24.0 (double)
Average聚合器
average聚合器用于返回数字集合所有元素的平均值。在类型上,average使用和sum一样的规则以最大程度的保证求值的精度。如果集合中的元素都不是数字,也和sum一样,抛出InvalidArgumentException。
ExpressionEvaluator.GetValue(null, "{1, 5, -4, 10}.average()") // 3 ExpressionEvaluator.GetValue(null, "{1, 5, -2, 10}.average()") // 3.5
Minimum聚合器
minimun聚合器返回列表元素中的最小值。为了确定“最小值”的真正含义,minimun要求目标集合中所有元素的类型相同并且都实现了IComparable接口。否则,该聚合器会抛出InvalidArgumentException异常。
ExpressionEvaluator.GetValue(null, "{1, 5, -3, 10}.min()") // -3 ExpressionEvaluator.GetValue(null, "{'abc', 'efg', 'xyz'}.min()") // 'abc'
Maximum聚合器
maximum聚合器返回列表元素中的最大值。为了确定“最大值”的真正意义,maximum要求目标集合中所有元素的类型相同并且实现了IComparable接口。否则,该聚合器会抛出InvalidArgumentException异常。
ExpressionEvaluator.GetValue(null, "{1, 5, -3, 10}.max()") // 10 ExpressionEvaluator.GetValue(null, "{'abc', 'efg', 'xyz'}.max()") // 'xyz'
nonNull处理器
nonNull处理器非常简单,作用是从集合中清除所有空值(null)。
ExpressionEvaluator.GetValue(null, "{ 'abc', 'xyz', null, 'abc', 'def', null}.nonNull()") // { 'abc', 'xyz', 'abc', 'def' } ExpressionEvaluator.GetValue(null, "{ 'abc', 'xyz', null, 'abc', 'def', null}.nonNull().distinct().sort()") // { 'abc', 'def', 'xyz' }
distinct处理器
distinct处理器可以确保集合中不包含重复的元素。该处理器还可以接收一个可选的布尔参数,用来决定结果中是否应该包含空值。该参数默认值是false,也就是说结果集合不包含空值。
ExpressionEvaluator.GetValue(null, "{ 'abc', 'xyz', 'abc', 'def', null, 'def' }.distinct(true).sort()") // { null, 'abc', 'def', 'xyz' } ExpressionEvaluator.GetValue(null, "{ 'abc', 'xyz', 'abc', 'def', null, 'def' }.distinct(false).sort()") // { 'abc', 'def', 'xyz' }
sort处理器
如果集合元素的类型统一且实现了IComparable接口,就可以用sort处理器进行排序。
ExpressionEvaluator.GetValue(null, "{1.2, 5.5, -3.3}.sort()") // { -3.3, 1.2, 5.5 } ExpressionEvaluator.GetValue(null, "{ 'abc', 'xyz', 'abc', 'def', null, 'def' }.sort()") // { null, 'abc', 'abc', 'def', 'def', 'xyz' }
引用容器中的对象
表达式也可以引用定义在IoC容器中的对象,语法为@contextName:objectName(按:contextName是指XML中<context>节点的名字,而非代码中的IApplicaitonContext变量的名称)。如果找不到以contextName命名的上下文,就使用根上下文的名字(Spring.RootContext)。可以参考IoC快速入门中的例子,下面的代码可以返回由Roberto Benigni指导的电影。(按:Roberto Benigni,罗伯特·贝尼尼,意大利著名演员,代表作有《美丽人生》、《皮诺曹》等,有不少是他自己编剧或导演的)
public static void Main() { . . . // Retrieve context defined in the spring/context section of // the standard .NET configuration file. IApplicationContext ctx = ContextRegistry.GetContext(); int numMovies = (int) ExpressionEvaluator.GetValue(null, "@(MyMovieLister).MoviesDirectedBy('Roberto Benigni').Length"); . . . }
最后,变量numMovies的值为2。
表达式
Lambda表达式是Spring.NET表达式语言中一项稍显复杂但非常强大的功能。Lambda表达式允许在表达式中定义内联函数,并且可以象真正的函数或方法一样在表达式中调用。
定义Lambda表达式的语法如下:
#
functionName = {|
argList|
functionBody }
例如,可以在表达式中定义一个max函数并调用它:
ExpressionEvaluator.GetValue(null, "(#max = {|x,y| $x > $y ? $x : $y }; #max(5,25))", new Hashtable()) // 25
可以看到,在函数体内引用前面定义的参数时,要使用本地变量的语法,即$加变量名。在调用时,将参数值以逗号分隔,列在函数名后的小括号内。
Lambda表达式支持递归,也就是说可以在函数体内部调用自身:
ExpressionEvaluator.GetValue(null, "(#fact = {|n| $n <= 1 ? 1 : $n * #fact($n-1) }; #fact(5))", new Hashtable()) // 120
请注意,在上面的两个例子中,我们必须为GetValue方法指定一个字典类型的参数,这里我们用哈希表。因为Lambda表达式实际上只是一些参数化变量,我们需要用一个字典来保存这些变量。如果不给GetValue方法指定一个有效的IDictionary实例,就会发生运行时错误。请参考10.3.10.节的相关内容。
另外,上面两个例子都是在同一表达式内定义和调用函数的。但一般来说我们希望定义了函数后能够在多个表达式中使用,Spring.NET通过一个静态方法Expression.RegisterFunction来预注册Lambda表达式,该方法接受函数名,Lambda表达式和存放变量的字典作为参数,如下:
IDictionary vars = new Hashtable(); Expression.RegisterFunction("sqrt", "{|n| Math.Sqrt($n)}", vars); Expression.RegisterFunction("fact", "{|n| $n <= 1 ? 1 : $n * #fact($n-1)}", vars);
一旦注册了函数,就可以在其它表达式中使用它们,注意要确保在每次求值时将字典变量vars传给了表达式求值引擎:
ExpressionEvaluator.GetValue(null, "#fact(5)", vars) // 120 ExpressionEvaluator.GetValue(null, "#sqrt(9)", vars) // 3
最后,请注意Lambda表达式本身也是按变量来处理的,它们可以被赋值给其它变量或作为参数传递给其他Lamba表达式。在下面的例子中,我们定义了一个委托函数,它有两个参数:第一个f要调用的函数,第二个n则是要传递给f的值。随后我们通过这个委托来调用前面注册过的两个函数,然后又调用了一个内联函数:
Expression.RegisterFunction("delegate", "{|f, n| $f($n) }", vars); ExpressionEvaluator.GetValue(null, "#delegate(#sqrt, 4)", vars) // 2 ExpressionEvaluator.GetValue(null, "#delegate(#fact, 5)", vars) // 120 ExpressionEvaluator.GetValue(null, "#delegate({|n| $n ^ 2 }, 5)", vars) // 25
虽然这个例子不算实用,但它确实证明了一点:Lambda表达式其实是参数化的变量,我们要牢记这一点。
空目标(Null Context)
如果没有在GetValue方法中指定根对象而使用了null(按:即第一个参数,前面已经有多处这样的用法)。那么表达式必须满足下面的某种情况:
-
字面值,如:ExpressionEvaluator.GetValue(null, "2 + 3.14");
-
引用某个类型的静态方法或字段,如:ExpressionEvaluator.GetValue(null,"DateTime.Today");
-
创建新的对象,如:ExpressionEvaluator.GetValue(null,"new DateTime(2004, 8, 14)");
-
或是引用其它对象,比如定义在变量词典或IoC容器中的对象。
后两种用法以后会专门讨论。
本章使用的示例类型
下面的类是在本章例子中使用的类型:
public class Inventor { public string Name; public string Nationality; public string[] Inventions; private DateTime dob; private Place pob; public Inventor() : this(null, DateTime.MinValue, null) {} public Inventor(string name, DateTime dateOfBirth, string nationality) { this.Name = name; this.dob = dateOfBirth; this.Nationality = nationality; this.pob = new Place(); } public DateTime DOB { get { return dob; } set { dob = value; } } public Place PlaceOfBirth { get { return pob; } } public int GetAge(DateTime on) { // not very accurate, but it will do the job ;-) return on.Year - dob.Year; } } public class Place { public string City; public string Country; } public class Society { public string Name; public static string Advisors = "advisors"; public static string President = "president"; private IList members = new ArrayList(); private IDictionary officers = new Hashtable(); public IList Members { get { return members; } } public IDictionary Officers { get { return officers; } } public bool IsMember(string name) { bool found = false; foreach (Inventor inventor in members) { if (inventor.Name == name) { found = true; break; } } return found; } }
下面是例子中使用的数据:
Inventor tesla = new Inventor("Nikola Tesla", new DateTime(1856, 7, 9), "Serbian"); tesla.Inventions = new string[] { "Telephone repeater", "Rotating magnetic field principle", "Polyphase alternating-current system", "Induction motor", "Alternating-current power transmission", "Tesla coil transformer", "Wireless communication", "Radio", "Fluorescent lights" }; tesla.PlaceOfBirth.City = "Smiljan"; Inventor pupin = new Inventor("Mihajlo Pupin", new DateTime(1854, 10, 9), "Serbian"); pupin.Inventions = new string[] {"Long distance telephony & telegraphy", "Secondary X-Ray radiation", "Sonar"}; pupin.PlaceOfBirth.City = "Idvor"; pupin.PlaceOfBirth.Country = "Serbia"; Society ieee = new Society(); ieee.Members.Add(tesla); ieee.Members.Add(pupin); ieee.Officers["president"] = pupin; ieee.Officers["advisors"] = new Inventor[] {tesla, pupin};