您的位置:首页 > 其它

NSPredicate(过滤器)

2015-03-11 14:23 211 查看
第十七章  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运算符。赋给该运算符一个正则表达式,谓词将会计算出他的值。

=============================================================================

NSPredicate是一个Foundation类,它指定数据被获取或者过滤的方式。它的查询语言就像SQL的WHERE和正则表达式的交叉一样,提供了具有表现力的,自然语言界面来定义一个集合被搜寻的逻辑条件。

相比较抽象的谈论它,展示NSPredicate的使用方法更加容易,所以我们来重新审视NSSortDescriptor 中使用的示例数据集吧:
1 2 3 4 名 Alice Bob Charlie Quentin 姓 Smith Jones
Smith Alberts 年龄 24 27 33 31
@ interface  Person : NSObject 
@property NSString *firstName; 
@property NSString *lastName; 
@property NSNumber *age; 
@end 
 
@implementation Person 
 
- (NSString *)description { 
     return  [NSString stringWithFormat:@ "%@ %@" , self.firstName, self.lastName]; 

 
@end 
 
#pragma mark -  
 
NSArray *firstNames = @[ @ "Alice" , @ "Bob" , @ "Charlie" , @ "Quentin"  ]; 
NSArray *lastNames = @[ @ "Smith" , @ "Jones" , @ "Smith" , @ "Alberts"  ]; 
NSArray *ages = @[ @24, @27, @33, @31 ]; 
 
NSMutableArray *people = [NSMutableArray array]; 
[firstNames enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 
    Person *person = [[Person alloc] init]; 
    person.firstName = firstNames[idx]; 
    person.lastName = lastNames[idx]; 
    person.age = ages[idx]; 
    [people addObject:person]; 
}]; 
 
NSPredicate *bobPredicate = [NSPredicate predicateWithFormat:@ "firstName = 'Bob'" ]; 
NSPredicate *smithPredicate = [NSPredicate predicateWithFormat:@ "lastName = %@" , @ "Smith" ]; 
NSPredicate *thirtiesPredicate = [NSPredicate predicateWithFormat:@ "age >= 30" ]; 
 
// ["Bob Jones"]  
NSLog(@ "Bobs: %@" , [people filteredArrayUsingPredicate:bobPredicate]); 
 
// ["Alice Smith", "Charlie Smith"]  
NSLog(@ "Smiths: %@" , [people filteredArrayUsingPredicate:smithPredicate]); 
 
// ["Charlie Smith", "Quentin Alberts"]  
NSLog(@ "30's: %@" , [people filteredArrayUsingPredicate:thirtiesPredicate]); 

集合中使用NSPredicate

Foundation提供使用谓词(predicate)来过滤NSArray/NSMutableArray&NSSet/NSMutableSet的方法。

不可变的集合,NSArray&NSSet,有可以通过评估接收到的predicate来返回一个不可变集合的方法filteredArrayUsingPredicate:和filteredSetUsingPredicate:。

可变集合,NSMutableArray&NSMutableSet,可以使用方法filterUsingPredicate:,它可以通过运行接收到的谓词来移除评估结果为FALSE的对象。

NSDictionary可以用谓词来过滤它的键和值(两者都为NSArray对象)。NSOrderedSet可以由过滤的NSArray或NSSet生成一个新的有序的集,或者NSMutableSet可以简单的removeObjectsInArray:,来传递通过否定predicate过滤的对象。

Core Data中使用NSPredicate

NSFetchRequest有一个predicate属性,它可以指定管理对象应该被获取的逻辑条件。谓词的使用规则在这里同样适用,唯一的区别在于,在管理对象环境中,谓词由持久化存储助理(persistent store coordinator)评估,而不像集合那样在内存中被过滤。

谓词语法

替换

%@是对值为字符串,数字或者日期的对象的替换值。

%K是key path的替换值。
NSPredicate *ageIs33Predicate = [NSPredicate predicateWithFormat:@ "%K = %@" , @ "age" , @33]; 
 
// ["Charlie Smith"]  
NSLog(@ "Age 33: %@" , [people filteredArrayUsingPredicate:ageIs33Predicate]); 

$VARIABLE_NAME是可以被NSPredicate -predicateWithSubstitutionVariables:替换的值。
NSPredicate *namesBeginningWithLetterPredicate = [NSPredicate predicateWithFormat:@ "(firstName BEGINSWITH[cd] $letter) OR (lastName BEGINSWITH[cd] $letter)" ]; 
 
// ["Alice Smith", "Quentin Alberts"]  
NSLog(@ "'A' Names: %@" , [people filteredArrayUsingPredicate:[namesBeginningWithLetterPredicate predicateWithSubstitutionVariables:@{@ "letter" : @ "A" }]]); 

基本比较

=, ==:左边的表达式和右边的表达式相等。

>=, =>:左边的表达式大于或者等于右边的表达式。

<=, =<:左边的表达式小于等于右边的表达式。

>:左边的表达式大于右边的表达式。

<:左边的表达式小于右边的表达式。

!=, <>:左边的表达式不等于右边的表达式。

BETWEEN:左边的表达式等于右边的表达式的值或者介于它们之间。右边是一个有两个指定上限和下限的数值的数列(指定顺序的数列)。比如,1 BETWEEN { 0 , 33 },或者$INPUT BETWEEN { $LOWER, $UPPER }。

基本复合谓词

AND, &&:逻辑与.

OR, ||:逻辑或.

NOT, !:逻辑非.

字符串比较

字符串比较在默认的情况下是区分大小写和音调的。你可以在方括号中用关键字符c和d来修改操作符以相应的指定不区分大小写和变音符号,比如firstname BEGINSWITH[cd] $FIRST_NAME。

BEGINSWITH: 左边的表达式以右边的表达式作为开始。

CONTAINS :左边的表达式包含右边的表达式。

ENDSWITH :左边的表达式以右边的表达式作为结束。

LIKE :左边的表达式等于右边的表达式:?和*可作为通配符,其中?匹配1个字符,*匹配0个或者多个字符。

MATCHES :左边的表达式根据ICU v3(更多内容请查看 ICU User Guide for Regular Expressions )的regex风格比较,等于右边的表达式。

合计操作

关系操作

ANY,SOME:指定下列表达式中的任意元素。比如,ANY children.age < 18。

ALL:指定下列表达式中的所有元素。比如,ALL children.age < 18。

NONE:指定下列表达式中没有的元素。比如,NONE children.age < 18。它在逻辑上等于NOT (ANY ...)。

IN:等于SQL的IN操作,左边的表达必须出现在右边指定的集合中。比如,name IN { 'Ben', 'Melissa', 'Nick' }。

数组操作

array[index]:指定数组中特定索引处的元素。

array[FIRST]:指定数组中的第一个元素。

array[LAST]:指定数组中的最后一个元素。

array[SIZE]:指定数组的大小。

布尔值谓词

TRUEPREDICATE:结果始终为真的谓词。

FALSEPREDICATE:结果始终为假的谓词。

NSCompoundPredicate

我们见过与&或被用在谓词格式字符串中以创建复合谓词。然而,我们也可以用NSCompoundPredicate来完成同样的工作。

例如,下列谓词是相等的:
[NSCompoundPredicate andPredicateWithSubpredicates:@[[NSPredicate predicateWithFormat:@ "age > 25" ], [NSPredicate predicateWithFormat:@ "firstName = %@" , @ "Quentin" ]]]; 
 
[NSPredicate predicateWithFormat:@ "(age > 25) AND (firstName = %@)" , @ "Quentin" ]; 

虽然语法字符串文字更加容易输入,但是在有的时候,你需要结合现有的谓词。在那些情况下,你可以使用NSCompoundPredicate -andPredicateWithSubpredicates:&-orPredicateWithSubpredicates:。

NSComparisonPredicate

同样的,如果你在读过 上周的文章 之后发现你使用了太多的NSExpression的话,NSComparisonPredicate可以帮助你解决这个问题。

就像NSCompoundPredicate一样,NSComparisonPredicate从子部件构建了一个NSPredicate--在这种情况下,左侧和右侧都是NSExpression。 分析它的类的构造函数可以让我们一窥NSPredicate的格式字符串是如何解析的:
+ (NSPredicate *)predicateWithLeftExpression:(NSExpression *)lhs 
                             rightExpression:(NSExpression *)rhs 
                                    modifier:(NSComparisonPredicateModifier)modifier 
                                        type:(NSPredicateOperatorType)type 
                                     options:(NSUInteger)options 

参数

lhs:左边的表达式。

rhs:右边的表达式。

modifier:应用的修改符。(ANY或者ALL)

type:谓词运算符类型。

options:要应用的选项。没有选项的话则为0。

NSComparisonPredicate类型
enum  { 
   NSLessThanPredicateOperatorType = 0, 
   NSLessThanOrEqualToPredicateOperatorType, 
   NSGreaterThanPredicateOperatorType, 
   NSGreaterThanOrEqualToPredicateOperatorType, 
   NSEqualToPredicateOperatorType, 
   NSNotEqualToPredicateOperatorType, 
   NSMatchesPredicateOperatorType, 
   NSLikePredicateOperatorType, 
   NSBeginsWithPredicateOperatorType, 
   NSEndsWithPredicateOperatorType, 
   NSInPredicateOperatorType, 
   NSCustomSelectorPredicateOperatorType, 
   NSContainsPredicateOperatorType, 
   NSBetweenPredicateOperatorType 
}; 
typedef NSUInteger NSPredicateOperatorType; 

NSComparisonPredicate选项

NSCaseInsensitivePredicateOption :不区分大小写的谓词。你通过在谓词格式字符串中加入后面带有[c]的字符串操作(比如,"NeXT" like[c] "next")来表达这一选项。

NSDiacriticInsensitivePredicateOption :忽视发音符号的谓词。你通过在谓词格式字符串中加入后面带有[d]的字符串操作(比如,"naïve" like[d] "naive")来表达这一选项。

NSNormalizedPredicateOption :表示待比较的字符串已经被预处理了。这一选项取代了NSCaseInsensitivePredicateOption和NSDiacriticInsensitivePredicateOption,旨在用作性能优化的选项。你可以通过在谓词格式字符串中加入后面带有
的字符串(比如,"WXYZlan" matches
".lan")来表达这一选项。

NSLocaleSensitivePredicateOption :表明要使用<,<=,=,=>,> 作为比较的字符串应该使用区域识别的方式处理。你可以通过在<,<=,=,=>,>其中之一的操作符后加入[l](比如,"straße" >[l] "strasse")以便在谓词格式字符串表达这一选项。

Block谓词

最后,如果你实在不愿意学习NSPredicate的格式语法,你也可以学学NSPredicate +predicateWithBlock:。
NSPredicate *shortNamePredicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 
             return  [[evaluatedObject firstName] length] <= 5; 
        }]; 
 
// ["Alice Smith", "Bob Jones"]  
NSLog(@ "Short Names: %@" , [people filteredArrayUsingPredicate:shortNamePredicate]); 

...好吧,虽然使用predicateWithBlock:是懒人的做法,但它也并不是一无是处。

事实上,因为block可以封装任意的计算,所以有一个查询类是无法以NSPredicate格式字符串形式来表达的(比如对运行时被动态计算的值的评估)。而且当同一件事情可以用NSExpression结合自定义选择器来完成时,block为完成工作提供了一个方便的接口。

重要提示:由predicateWithBlock:生成的NSPredicate不能用于由SQLite存储库支持的Core Data数据的提取要求。

我知道我已经说过很多次了,可是NSPredicate真的是Cocoa的优势之一。其他语言的第三方库如果能有它一半的能力就已经很幸运了--更别提标准库了。对于我们这些应用和框架开发者来说,有它作为标准组件使得我们在处理数据时有了很大的优势。

和NSExpression一样,NSPredicate一直在提醒我们Foundation有多么好:它不仅仅十分有用,它精致的构架和设计也是我们写代码时灵感的来源。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  NSPredicate