第二十章、NSPredicate

  • 编写软件时,经常需要获取一个对象集合,并通过某些已知条件计算该集合的值。你需要保留符合某个条件的对象,删除那些不满足条件的对象,从而提供一些有用的对象

  • Cocoa提供了一个名为NSPredicate的类,它用于指定过滤器的条件。可以创建NSPredicate对象,通过它准确的描述所需的条件,通过谓词筛选每个对象,判断它们是否与条件相匹配。

  • 这种意义上的“谓词”与在英语语法课上学习的“谓语”大不相同。这里的谓语用在数学和计算机科学概念中,表示计算真值和假值的函数。

  • Cocoa用NSPredicate描述查询的方式,原理类似于在数据库中进行查询。可以在数据库风格的API中使用NSPredicate类,比如Core Data和Spotlight。此外,我们不打算介绍这两种技术(但可以将本章中的很多内容应用到这两种技术中,也可以应用到自己的对象中)。可以将NSPredicate看成另一种间接操作方式。例如,如果需要查询满足条件的机器人,可以使用谓词对象进行检查,而不必使用代码进行显示查询。通过交换谓词对象,可以使用通用代码对数据进行过滤,而不必对相关条件进行硬编码。

1.创建谓词

  • 首先需要创建NSPredicate对象,才能将它应用于其他对象。可以通过两种基本方式来实现。第一种是创建许多对象,并将它们组合起来。这需要使用大量代码,如果你正在构建通用用户界面来指定查询,采用这种方式比较简单。另一种方式是查询代码中的字符串。对于初学者来说,这种方式比较简单。因此我们详细介绍查询字符串。通常建议使用面向字符串的API,尤其是在缺少编译器的错误检查和奇怪的运行时错误的时候。

  • 我们使用之前的Car示例,首先我们来看看这辆车的情况。

Car *car;
car = makeCar (@"Herbie", @"Honda", @"CRX", 1984, 2, 110000, 58);
[garage addCar: car];
  • 之前我们已经编写了makeCar函数,它可以创建一辆汽车,并为其加上引擎和一些车胎。

  • 现在创建谓词:

NSPredicate *predicate;
predicate = [NSPredicate predicateWithFormat:@"name == 'Herbie'];
  • 我们将以上代码拆开分析。predicate是一个普通的Objective-C对象指针,它将指向NSPre对象。我们使用NSPredicate的类方法+predicateWithFormat:来创建一个真实的谓词。将某个字符串付给谓词,+predicateWithFormat:使用该字符串在后台构建对象数,来计算谓词的值。

  • predicateWithFormat方法听起来很像stringWithFormat方法,后者由NSString类提供,可以通过它使用print样式的格式说明符来插入某些内容。稍后你将看到,也可以使用predicateWithFormat方法实现相同的功能。Cocoa采用一致的命名模式,最好遵循它。

  • 这种谓词字符串看上去像是标准的C语言表达式。它的左侧是键路径name,随后是一个等于运算符“==”,右侧是一个用单括号括起来的字符串。如果谓词字符串中的这段文本没有打引号,就会被当做键路径。只有打了引号,它才能被当做字符串的字面量来处理。可以使用单引号也可以双引号(只要前后匹配就好)。通常,还是应该使用单引号,否则必须在字符串中对每一个双引号进行转移。

2.计算谓词

  • 通过以上步骤就可以得到一个谓词。接下来做什么呢?通过某个对象来计算它。
BOOL match = [predicate evaluateWithObject:car];
NSLog(@"%s",(match) ? "YES" : "NO");
  • -evaluateWithObject:通知接收对象(即谓词)根据指定的对象计算自身的值。在本例中,接收对象为car,使用name作为键路径,应用valueForKeyPath:方法获取名称。然后,它将自身的值(即名称)与Herbie相比较。如果名称和Herbie相同,则-evaluateWithObject:返回YES,否则返回NO。此处,NSLog使用三元运算符将数值BOOL转换成人们可读的字符串形式。

  • 以下是另一个谓词:

predicate = [NSPredicate predicateWithFormat:@"engine.horsepower > 150"];
match = [predicate evaluateWithObject:car];
  • 此谓词字符串左侧是一个键路径。该路径链接到汽车内部,查找引擎,然后查找引擎的马力。接下来,它将马力值与150进行比较,看它是否更大。

  • 通过Herbie计算出这些内容后,得到match值为NO,因为小型Herbie的马力值(58)小于150。

  • 通过特定的谓词条件检查单个对象时进展都很顺利,如果需要检查对象集合,情况就会变得更加有趣。假设我们需要查看车库中那些汽车的功率最大,可以循环测试每个汽车的谓词。

NSArray *cars = [garage cars];
for (Car *car in [garage cars]) {
     if ([predicate evaluateWithObject:car]) {
          NSLog(@"%@",car.name);
      }
}
  • 从车库中获取所有汽车,对它们进行循环,通过谓词计算每个汽车的马力。以上代码将输出较高马力的汽车:
Elvis
Phoenix
Judge
  • 在继续介绍以下内容之前,我们先要确保理解了这里涉及的所有语法。仔细查看NSLog中关于汽车名称的调用。car.name与[car name]是等效的。这里的谓词字符串"engine.horsepower > 150",engine.horsepower是键路径,它会在后台执行各种强大的功能。

3.数组过滤器

  • 某些类别将谓词过滤方法添加到Cocoa集合类中,-filteredArrayUsingPredicate:是NSArray数组中的一种类别方法,它将循环过滤数组内容,根据谓词计算每个对象的值,并将值为YES的对象累计到将被返回的新数组中。
NSArray *results;
results = [cars filteredArrayUsingPredicate:predicate];
NSLog(@"%@",results);
  • 这些代码将输出以下结果:
(
Elvis, a 1989 Acura Legend, has 4 doors, 28123.4 miles, 151 hp and 4 tires,
Phoenix, a 1969 Pontiac Firebird, has 2 doors, 85128.3 miles, 345 hp and 4 tires,
Judge, a 1969 Pontiac GTO, has 2 doors, 45132.2 miles, 370 hp and 4 tires
)
  • 以上这些结果同前面的结果不一样,这里是一组汽车的信息,在前面的示例中,我们得到的结果是汽车名称。我们可以使用KVC(键/值编码)提取其中的名称。请记住,将valueForKey:发送给数组时,键将作用于数组中的所有元素。
NSArray *names;
names = [results valueForKey:@"name"];
NSLog(@"%@"names);
  • 如果你有一个可变数组,而且需要剔除不属于该数组的所有项目。NSMutableArray具有-filterUsingPredicate方法,它能轻松实现你的目标。
NSMutableArray *carsCopy = [cars mutableCopy];
[carsCopy filterUsingPredicate:predicate];
  • 如果你输出carCopy,结果将是前面我们看到的3辆汽车的集合。

  • 因为NSMutableArray是NSArray的子类,所以你也可以对NSMutableArray数组使用-filterUsingPredicate方法来构建新的不可变数组。NSSets中也有类似的调用方法。

  • 我们在讨论KVC时提到过,使用谓词确实很便捷,但它的运行速度不会比你自己编写全部代码快。因为它无法避免在所有汽车之间使用循环和对每辆汽车进行某些操作。一般来说,这种循环并不会对OS X上应用的性能产生很大的影响,因为当今的计算机运行速度非常快。尽量编写最便捷的代码。如果遇到了速度问题,可以使用苹果公司的工具(比如Instruments)来测试程序性能,不过iOS程序员应该随时密切关注程序的性能。

4.格式说明符

  • 资深编程人员都知道,硬编码并非好办法。如果首先想知道那些汽车的马力高于200,稍后又需要知道那些汽车的马力高于50,该怎么办?可以使用谓词字符串,例如"engine.horsepower > 200"和"engine.horsepower > 50",但我们必须重新编译程序,并会再次遇到之前的麻烦问题。

  • 可以通过两种方式将不同的内容放入谓词格式字符串中:格式说明符和变量名。首先介绍格式说明符。可以在你熟知的%d和%f格式说明符中使用数字形式的值。

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:@"Herbies",@"NAME",nil];
  • 这里使用字符串"Herbie"作为键"NAME"的值。因此,构造以下形式的新谓词。
predicate = [predicateTemplate predicateWithSubstitutionVariables: varDict];
  • 该谓词的工作方式和你之前所见过的其它谓词完全相同。你也可以使用其他对象作为变量的值,例如NSNumber。以下谓词用于过滤引擎的功率。
predicateTemplate = [NSPredicate predicateWithFormat:@"engine.horsepower > $POWER"];
varDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt: 150],@"POWER",nil];
predicate = [predicateTemplate predicateWithSubstitutionVariables: varDict];
  • 以上代码创建了一个谓词,它的条件是引擎功率要大于150。

  • 除了使用NSNumber和NSString之外,也可以使用[NSNull null]来设置nil值,甚至可以使用数组,这些内容稍后部分再说。请注意,不能使用“$变量名”作为键路径,它只能表示值。使用谓词格式字符串时,如果想要在程序中通过代码改变键路径,需要使用%k格式说明符。

  • 谓词机制不进行静态型检查。你也许会在要输入数字的地方不小心插入字符串,这样就会出现运行时错误信息,或者其它不可预知的行为。

5.运算符

  • NSPredicate的格式字符串包含大量不同的运算符。这里我们将介绍大多数运算符,并给出每个运算符的示例。其余运算符可以通过苹果公司的在线文档进行查询。

5.1比较和逻辑运算符

  • 谓词字符串语法支持C语言中的一些常用的运算符,例如等号运算符“==”和“=”等

  • 而不等号运算符具有多种形式,如下所示:

>            大于某数
>=和=>       大于或等于某数
<            小于某数
<=和=<       小于或等于某数
!=和<>        不等于某数
  • 此外,谓词字符串语法还支持括号表达式(真的!)和AND,OR和NOT逻辑运算符,以及用C语言样式表示具有相同功能的“&&”,“||”,“!” 符号。

  • 以下是一个示例。你可以将功率最大和最小的汽车过滤掉,留下中等功率的汽车。

predicate = [NSPredicate predicateWithFormat:@"(engine.horsepower > 50) AND (engine.horsepower < 200)"];
results = [cars filteredArrayUsingPredicate:predicate];
NSLog(@"oop %@",results);
  • 如果将以上代码应用到汽车中,将得到以下结果。
Herbie, a 1984 Honda CRX, has 2 doors, 34000.0 miles, 58 hp and 4 tires,
Badger, a 1987 Acura Integra, has 5 doors, 217036.7 miles, 130 hp and 4 tires,
Elvis, a 1989 Acura Legend, has 4 doors, 28123.4 miles, 151 hp and 4 tires,
Paper Car, a 1965 Plymouth Valiant, has 2 doors, 76800.0 miles, 105 hp and 4 tires
  • 谓词字符串中的运算符不区分大小写。你可以随意使用And,ANd和AND。这里我们将统一使用大写字母,但在实际代码中可以不区分大小写。

  • 不等号既适用于数字值又适用于字符串值。如果需要按字母表顺序从头查看所有汽车,可以使用以下谓词。

predicate = [NSPredicate predicateWithFormat:@" name < 'Newton' "];
results = [cars filteredArrayUsingPredicate:predicate];
NSLog(@"%@",[results valueForKey:@"name"]);
  • 将会输出以下结果:
(
   Herbie,
   Badger,
   Elvis,
   Judge
)

5.运算符

5.2数组运算符

  • 谓词字符串"(engine.horsepower > 50) OR (engine.horsepower < 200)"是一种非常常见的模式,该谓词字符串用于查找介于50到200之间的马力值。如果我们能使用某个运算符来查找介于这两个值之间的数值。我们可以使用以下代码来实现该功能。
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'} "];
  • 名称为Herbie和Badger的汽车将会在过滤中存留下来。
results = [cars filteredArrayUsingPredicate:predicate];
NSLog(@"%@",[results valueForKey:@"name"]);
  • 已经看到结果了,只有以下两个对象被返回:
(
   Herbie,
   Badger
)

6.SELF

  • 某些时候,可能需要将谓词应用于简单的值(例如纯文本的字符串),而非那些可以通过键路径进行操作的复杂对象。假设,我们有一个汽车名称的数组,并且需要应用于之前相同的过滤器。从NSString对象中查询name时,将无法起作用,那么我们用什么来代替name呢?

  • 用SELF就可以解决了!SELF表示的是响应谓词计算的对象。事实上我们可以将谓词中所有的键路径表示成对应的SELF形式。此谓词和前面的谓词完全相同,代码如下所示。

predicate = [NSPredicate predicateWithFormat:@" SELF.name IN {'Herbie','Snugs','Badger','Flap'} "];
  • 现在,再回到那个字符串数组。如果某个字符串数组也在这个名称数组中,该怎么办呢?

  • 首先,需要从某处获取仅含有名称的数组。对数组使用KVC技术的valueForKey:方法就可以获取到了。

name = [cars valueForValue:@"name"];
  • 以上字符串数组包含我们拥有的所有汽车的名称。接下来构造一个谓词
predicate = [NSPredicate predicateWithFormat:@" SELF IN {'Herbie','Snugs','Badger','Flap'} "];
  • 并计算该谓词的值。
results = [names filteredArrayUsingPredicate:predicate];
  • 如果现在查看结果将会看到和前面示例相同的两个名称:Herbie和badger。

  • 这里提一个问题,以下代码将输出什么结果呢?

NSArray *name1 = [NSArray arrayWithObjects:@"Herbie",@"Badger",@"Judge",@"Elvis",nil];
NSArray *name2 = [NSArray arrayWithObjects:@"Judge",@"Paper Car",@"Badger",@"Phoenix",nil];
predicate = [NSPredicate predicateWithFormat:@"SELF IN %@",name1];
results = [names2 filteredArrayUsingPredicate:predicate];
NSLog(@"%@",results);
  • 答案如下所示:
(
  Judge,
   Badger
)
  • 对于取两个数组交集的运算而言,这是一种很巧妙的方式。但它是如何实现的呢?谓词包含了第一个数组的内容,因此看起来和下面的形式类似。
SELF IN {"Herbie","Badger","Judge","Elvis"}
  • 现在,使用该谓词过滤第二个名称数组。在name2中如果具有同时存在两个数组中的字符串,那么SELF IN语句会确定它是符合条件的,因此它就会保留在结果数组中。如果对象只存在于第二个数组中,那么它不会与谓词中的任何字符串匹配,所以该对象将被过滤掉。而只存在于第一个数组中的字符串因为要用来进行比较,所以将一直保留在原来的位置,不会出现在结果数组中。

7.字符串运算符

  • 前面使用字符串时介绍过关系运算符。此外,还有一些针对字符串的关系运算符。
    1.BEGINSWITH:检查某个字符串是否以另一个字符串开头
    2.ENDSWITH:检查某个字符串是否以另一个字符串结尾
    3.CONTAINS:检查某一个字符串是否在另一个字符串内部

  • 使用关系运算符可以执行一些有用的操作,例如使用"name BEDGINSWITH 'Bad' "匹配Badger,使用"name ENDSWITH 'vis' "匹配Elvis,以及使用"name CONTAINS udg"匹配Judge。

  • 如果你编写了某个类似于"name ENDSWITH 'HERB' "的谓词字符串,会出现什么情况呢?它不会与Herbie或其他字符串相匹配,因为这些匹配是区分大小写的。同样"name BEGINSWITH 'Hérb' "也不会与之相匹配,因为其中的e含有重音符。为了减少名称匹配规则,可以为这些运算符添加[c]、[d]或[cd]修饰符。其中,c表示“不区分大小写”,d表示“不区分发音符号”(即忽略重音符),[cd]表示“即不区分大小写,也不区分发音符号”

  • 通常,除非你拥有区分大小写或重音符号的特殊原因,否则请尽量使用[cd]修饰符。你无法预知用户什么时候会按下大写锁定致使程序输入的文字全变成了大写。

  • 该谓词字符串会将Herbie与"name BEGINSWITH[cd] 'HERB' "相匹配。

8.LIKE运算符

  • 某些时候,将一个字符串的开头或结尾(也可能是中间)与另一个字符串进行匹配的功能还不够。对于这种情况,谓词格式字符串还提供了Like运算符。在该运算符中,问号表示与一个字符匹配,星号表示与任意个字符匹配。SQL和Unix shell编程人员应该认识这种操作(有时称为“通配符”)

  • 谓词字符串"name LIKE 'er' "将会与任何含有er的名称相匹配。这等效于CONTAINS。**

  • 谓词字符串"name LIKE '???er' "将会与Paper Car相匹配。因为其中的er前面有4个字符。*

  • 另外,LIKE也接受[cd]修饰符,用于忽略对大小写和发音符号的区分。

  • 如果你热衷于正则表达式,可以使用MATCHES运算符。赋给它一个正则表达式,谓语将会计算出它的值。

  • 正则表达式功能非常强大,它是一种指定字符串匹配逻辑的非常紧凑的方式。有时候,正则表达式的形式会变得复杂而费解,已经有大量书讨论这一主题。NSPredicate正则表达式使用ICU语法,你可以借助因特网搜索引擎了解相关内容。

  • 虽然正则表达式的功能强大,但计算开销非常大。如果在谓词中有某些简单的运算符,例如基本字符串运算符和比较运算符,那么在使用MATCHES之前可以先执行简单的运算,这样将会提高程序的运算速度。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容