第十七章 NSPredicate
编写软件时,经常需要获取一个对象集合,然后删除不满足条件的对象,保留符合条件的对象,从而提供一些有意义的对象。
Cocoa提供了一个名为NSPredicate的类,他用于指定过滤器的条件。可以创建NSPredicate对象,通过该对象准确地描述所需的条件,对每个对象通过谓词进行筛选,判断他们是否与条件相匹配。
Cocoa用NSPredicate描述查询的方式,原理类似于在数据库中进行查询。可以在数据库风格的API中使用NSPredicate类,例如Core Data和Spotlight。可以将NSPredicate看成另一种间接操作方式。例如,如果需要查询满足条件的机器人,可以使用谓词对象进行检查,而不必使用代码进行显式查询。通过交换谓词对象,可以使用通用代码对数据进行过滤,而不必对相关条件进行硬编码。
这也是开放/关闭原则的另一个应用。
17.1 创建谓词
可以通过两种基本方式来实现。
第一种是创建许多对象,并将他们结合起来。还需要使用大量代码,如果正在构建通用用户接口来指定查询,采用这种方式比较简单。
另一种方式是查询代码中的字符串。对初学者来说,这种方式比较简单。因此,本书中我们终点介绍查询字符串。常见的面向字符串的API警告信息适用于查询字符串,特别适用于缺少编译器错误检查及有时出现奇怪的运行时错误等情况。
我们仍然使用CarParts示例,本章的示例基于上一章创建的汽车车库示例。
首先看一下一辆汽车的情况:
Car *car;
car=makeCar(@"Herbie",@"Honda",@"CRX",1984,2,110000,58);
[garage addCar:car];
现在创建谓词:
NSPredicate *predicate;
predicate=[NSPredicate predicateWithFormat:@"name== 'Herbie'"];
+predicateWithFormat:来实际创建谓词。可以使用单引号,双引号需要进行转义
计算谓词
BOOL match=[predicate evaluateWithObject:car];
NSLog(@"%s",(match)?"YES":"NO");
-evaluateWithObject:通知接收对象根据指定的对象计算自身的值。在本例中,接收对象为car,使用name做为键路径,应用valueForKeyPath:方法获取名称。然后,他将自身的值与"Herbie"相比较。如果名称和"Herbie"相同,则-evaluateWithObject:返回YES,否则返回NO。
以下是另一个谓词:
predicate=[NSPredicate predicateWithFormat:@"engine.horsepower>150"];
match=[predicate evaluateWithObject:car];
如果检查对象集合,情况会变得更加有趣:
NSArray *cars=[garage cars];
for (Car *car in cars) {
if ([predicate evaluateWithObject:car]){
NSLog(@"%@",car.name);
}
}
合理吗?不!在继续介绍以下内容之前,我们先要确保理解了这里涉及的所有语法。仔细查看NSLog中的汽车名称调用。他使用了Obj-C 2.0的点语法,这与调用[car name]是等效的。
17.2 燃料过滤器
编程人员最显著的优点/缺点都是懒惰。如果我们不必编写for循环和if语句,这有什么不好吗?幸运的是,某些类别将谓词过滤方法添加到了Cocoa集合类中。
-filteredArrayUsingPredicate:是NSArray数组中的一个类别方法,他将循环过滤数组内容,根据谓词计算每个对象的值,并将值为YES的对象累积到将被返回的新数组中:
NSArray *results;
results=[cars filteredArrayUsingPredicate:predicate];
NSLog(@"%@",results);
以上这些结果同前面的结果不一样,这里是一组汽车,前面是一组名称,记住valueForKey:发送给数组时,键将作用于数组中的所有元素:
NSArray *names;
names=[results valueForKey:@"name"];
NSLog(@"%@",names);
假设有一个可变数组,你需要剔除不属于该数组的所有项目。NSMutableArray具有-filterUsingPredicate方法,他能轻松实现你的目标:
NSMutableArray *carsCopy=[cars mutableCopy]; // 将数组拷贝一份
[carsCopy filterUsingPredicate:predicate];
如果输出carsCopy,结果将是前面我们看到的3辆汽车的集合。
也可以使用-filteredArrayUsingPredicate:方法和NSMutableArray数组来构建新数组,因为NSMutableArray是NSArray的超类。
NSSets中也有类似的调用方法。
正如我们在讨论KVC时提到的,使用谓词确实很便捷,但是性能上会差一些,尤其是开发iPhone程序,应该随时密切关注程序的性能
17.3 格式说明符
如果需要知道哪些汽车的马力高于200,稍后又需要知道哪些汽车的马力高于50,我们该怎么办?硬编码不是一个好办法。我们可以使用谓词字符串。例如“engine.horsepower>200”和“engine.horsepower>50”,但我们必须重新编译程序,并再次遇到第三章中的麻烦问题。
可以通过两种方式将不同的内容放入谓词格式字符串中:格式说明符和变量名。首先,我们将介绍格式说明符。
predicate=[NSPredicate predicateWithFormat:@"engine.horsepower>%d",50];
当然,我们一般不直接在代码中使用常量值50.
除了使用printf说明符,也可以使用%@插入字符串值,将%@看成是一个引用字符串:
predicate=[NSPredicate predicateWithFormat:@"name==%@",@"Herbie"];
注意,这里的格式字符串中没有引用%@。如果你需要引用%@,例如"name='%@'",应该将%@放在谓词字符串中。
通过NSPredicate字符串,也可以使用%K指定键路径。该谓词和其他谓词相同,使用name=='Herbie'做为条件:
predicate=[NSPredicate predicateWithFormat:@"%K==%@",@"name",@"Herbie"];
NSPredicate *predicateTemplate=[NSPredicate predicateWithFormat:@"name==$NAME"];
现在,我们有一个含有变量的谓词。接下来,可以使用predicateWithSubstitutionVariables调用来构建新的专用谓词,创建一个键/值对字典。其中,键是变量名(不包含美元符号$),值是插入谓词的内容,代码如下搜索:
NSDictionary *varDict;
varDict=[NSDictionary dictionaryWithObjectsAndKeys:@"Herbie",@"NAME",nil];
predicate=[predicateTemplate predicateWithSubstitutionVariables:varDict];
可以使用不同的对象做为变量名称,例如NSNumber。以下谓词用于过滤引擎的功率:
predicateTemplate=[NSPredicate predicateWithFormat:@"engine.horsepower>$POWER"];
varDict=[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:150],@"POWER",nil];
predicate=[predicateTemplate predicateWithSubstitutionVariables:varDict];
除了使用NSNumber和NSString之外,也可以使用[NSNull null]设置nil值,甚至可以使用数组。
注意,不能用$VARIABLE作为键路径,因为他只表示值。使用谓词格式字符串时,如果想通过程序改变键路径,需要使用$K格式说明符。
谓词机制不执行类型检查。你也许会在输入数字的地方不小心插入字符串,这样就会出现运行时错误信息,或者出现其他不可预知的错误。
17.4 运算符
NSPredicate的格式字符串包含大量不同的运算符,这里将介绍大多数。其余的运算符可以通过苹果公司的在线文档进行查询。
17.4.1 比较和逻辑运算符
==和=、>、>=和=>、<、<=和=<、!=和<>
此外,谓词字符串语法还支持括号表达式和AND、OR、NOT逻辑运算符或者C样式的等效表达式&&、||和!
示例:
predicate=[NSPredicate predicateWithFormat:@"(engine.horsepower>50) AND
(engine.horsepower<200)"];
谓词字符串中的运算符不区分大小写。你可以随意使用AnD、And或or。
不等号既适用于数字值又适用于字符串值。如果需要按字母顺序从开始查看所有汽车,可以使用以下谓词:
predicate=[NSPredicate predicateWithFormat:@"name<'Newton'"];
results=[cars filteredArrayUsingPredicate:predicate];
NSLog(@"%@",[results valueForKey:@"name"]);
17.4.2 数组运算符
predicate=[NSPredicate predicateWithFormat:@"engine.horsepower BETWEEN {50,200}"];
花括号表示数组,BETWEEN将数组中的第一个元素看成是数组的下界,第二个元素看成是数组的上界。
可以使用%@格式说明符插入你自己的NSArray对象:
NSArray *betweens=[NSArray arrayWithObjects:
[NSNumber numberWithInt:50],
[NSNumber numberWithInt:200],nil];
predicate=[NSPredicate predicateWithFormat:@"engine.horsepower BETWEEN %@",betweens];
也可以使用变量:
predicateTemplate=[NSPredicate predicateWithFormat:@"engine.horsepower BETWEEN $POWERS"];
varDict=[NSDictionary dictionaryWithObjectsAndKeys:betweens,@"POWERS",nil];
predicate=[predicateTemplate predicateWithSubstitutionVariables:varDict];
数组并不仅仅用来指定某个区间的端点值,你可以使用IN运算符查找数组中是否含有某个特定值,具有SQL编程经验的编程人员应该对以下代码非常熟悉:
predicate=[NSPredicate predicateWithFormat:@"name IN {'Herbie','Snugs','Badger','Flap'}"];
17.5 SELF 足够了
某些时候,可能需要将谓词应用于简单的值(例如那些纯文本老式字符串),而并非那些可以通过键路径进行操作的对象。假设我们有一个汽车名称数组,并且需要应用前面相同的过滤器,从NSString对象中查询name时,将不能起到预期效果。那么,我们用什么来代替name呢?
用SELF来解决!SELF可以引用用于谓词计算的对象。事实上,我们可以将谓词中所有的键路径表示成对应的SELF。此谓词和前面的谓词完全相同,代码如下所示:
predicate=[NSPredicate predicateWithFormat:@"SELF.name IN {'Herbie','Snugs','Badger','Flap'}"];
现在,再回到那个字符串数组。如果某个字符串也在名称数组中,该怎么办呢?我们来分析一下。
首先,需要从某处获取仅含有名称的数组,因为已经熟悉了CarParts中的各种对象,因此,我们将借助于数组,使用KVC技术获取valueForKey:方法,以便处理这些对象:
NSArray *names=[cars valueForKey:@"name"];
predicate=[NSPredicate predicateWithFormat:@"SELF IN {'Herbie','Snugs','Badger','Flap'}"];
results=[names filteredArrayUsingPredicate:predicate];
这里有一个问题,以下代码将输出什么结果呢?
NSArray *names1=[NSArray arrayWithObjects:@"Herbie",@"Badger",@"Judge",@"Elvis",nil];
NSArray *names2=[NSArray arrayWithObjects:@"Judge",@"Paper Car",@"Badger",@"Finto",nil];
predicate=[NSPredicate predicateWithFormat:@"SELF IN %@",names1];
results=[names2 filteredArrayUsingPredicate:predicate];
NSLog(@"%@",results);
输出:
{
Judge,
Badger
}
对于取两个数组的交集的运算而言,这是一种很巧妙的方式。
17.6 字符串运算符
前面介绍字符串时,我们介绍过关系运算符。此外,还有一些针对字符串的关系运算符:
BEGINSWITH
ENDSWITH
CONTAINS
例如:
"name BEGINSWITH 'Bad'" 匹配 "Badger" ,使用"name ENDSWITH 'vis'" 匹配 "Elvis",以及 "name CONTAINS 'udg'"
这些匹配是区分大小写的,也区分重音符。为了减少名称匹配规则,可以为这些运算符添加[c]、[d]或[cd]修饰符。其中,c表示“不区分大小写”,d表示“不区分发音符号”,[cd]表示都不区分。例如 "name BEGINSWITH[cd] 'HERB'"
17.7 LIKE运算符
通配符: ? 匹配单个字符 * 匹配任意个字符
"name LIKE '*er'" 匹配任何包含er的名称,等效于CONTAINS
"name LIKE '???er*'"
另外,LIKE也接受[cd]修饰符
如果你热衷于正则表达式,可以使用MATCHES运算符。赋给该运算符一个正则表达式,谓词将会计算出他的值。