NSPredicate 的简单使用
0x00 前言
最近在重新学习 Objective-C,因为觉得基础非常重要,而且对 Objective-C 的使用都基本上是工作中用到的,了解得还不够多,就算当初学习 Objective-C 的时候知道了,但是到现在也忘了不少东西。内容主要来自《Objective-C 基础教程(第2版)》,这本书的代码可以到这里下载。
0x01 NSPredicate
其实当初第一次接触 NSPredicate 的时候,觉得不是很懂,也觉得有点难,也没去多了解一下,直到最近重新学基础时才发觉,其实 NSPredicate 挺好用的,也很好理解,以前用的最多的应该是正则吧,各种语言里面都是可以用的,就是需要记住的符号什么的好多,如果长时间不用的话,就会忘记,虽然这很正常。
基本上每个 App 都会涉及到数据的筛选过滤,也就是搜索,很少见到哪个 App 没有搜索功能。Cocoa 用 NSPredicate 描述查询的方式,原理类似于数据库查询,其实感觉语法上也有点相似呢。
0x02 创建谓词
我们这里还是使用书上的例子,不过不用担心,就算没有看过书上的例子代码也是 OK 的,不难理解。
下面是一个快速创建一辆汽车的 C 函数:
1 | Car *makeCar(NSString *name, NSString *make, NSString *model, int modelYear, int numberOfDoors, float mileage, int horsepower) { |
可以使用这个 C 函数来创建一辆汽车:
1 | Car *car = makeCar(@"Herbie", @"Honda", @"CRX", 1984, 2, 34000, 58); |
上面代码创建一辆汽车,具体的汽车信息为:Herbie 品牌,型号为双门1984 Honda CRX,马力引擎为58,已行驶34000英里。
现在来创建谓词:
1 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == 'Herbie'"]; |
我们使用 NSPredicate 的类方法 +predicateWithFormat:
创建了一个 NSPredicate 的对象,并传入了一个字符串参数,这个类方法使用该字符串在后台构建对象,来计算谓词的值,简单来说,就是以这个字符串来当筛选过滤的条件。
0x03 计算谓词
通过以上步骤,就创建好了一个谓词,现在我们可以使用它了:
1 | BOOL match = [predicate evaluateWithObject:car]; |
方法 -evaluateWithObject:
通知接收对象(即谓词)根据指定的对象计算自身的值。这里是以 name 作为键路径,使用 -valueForKeyPath:
方法来获取名称,然后将自身的这个名称(即 name)与 Herbie 进行比较,相同返回 YES,否则返回 NO。
以下是另外一个谓词:
1 | predicate = [NSPredicate predicateWithFormat:@"engine.horsepower > 150"]; |
这个谓词字符串的左侧是一个键路径,该键路径链接到 car 内部,查找 engine,然后再查找 engine 的 horsepower,然后将其与150进行比较,看它是否大于150。
以上都是通过特定的谓词条件检查单个对象,都很简单,但是在实际工作当中,我们通常都是从一堆的数据里面进行筛选的,比如,有一个车库,我们需要筛选出哪些汽车的马力大于150,普通的做法可能如下:
1 | for (Car *car in [garage cars]) { |
使用 for 循环遍历每一辆汽车,然后检查其马力是否大于150,大于150的话,就输出汽车的名字。
0x04 数据过滤器
懒惰一直是编程人员的缺点,但是在某种意义上也是优点。如果不用编写这样的 for 循环和 if 语句,没什么不好的。而且 Cocoa 也提供了这样的方法:-filteredArrayUsingPredicate:
,这个方法是 NSArray 数组中的一种类别方法,它会循环遍历数组中的对象,根据谓词来计算每个对象的值,如果是 YES,那么就将这个对象添加到将被返回的新数组中,感觉这个和上面的那个 for 循环类似吧?
1 | NSArray *results = [cars filteredArrayUsingPredicate:predicate]; |
这样的话,只需要一行代码就可以获取筛选之后的结果了,不过这里输出的是汽车的全部信息,而不是上面那样输出汽车的名字,不过我们可以使用 KVC(键值编码)来提取其中的名称:
1 | NSArray *names = [results valueForKey:@"name"]; |
上面的 cars 是不可变数组,如果我们的数据是存放在可变数组里面,而且我们需要剔除不满足条件的对象的话,可以使用 NSMutableArray 的 -filterUsingPredicate:
方法:
1 | NSMutableArray *carsCopy = [cars mutableCopy]; |
因为 NSMutableArray 是 NSArray 的子类,所以也是可以用 -filteredArrayUsingPredicate:
方法来构建新的不可变数组的。使用谓词的确很便捷,但是它的运行速度并不会比自己编写全部代码要快,因为它无法避免要使用循环来遍历。
0x05 格式说明符
从方法 -predicateWithFormat:
可以看出来,传入的字符串是可以使用格式说明符的,比如:%d
、%f
等。而且一般筛选条件都不是硬编码到项目里面的,都是根据用户的输入来进行筛选的。
除了可以使用 printf 说明符,还可以使用 %@
来插入字符串,而 %@
会被当做带引号的字符串:
1 | predicate = [NSPredicate predicateWithFormat:@"name == %@", @"Herbie"]; |
要注意,这里的格式字符串 %@
并没有打单引号,如果打上了单引号,例如:"name == '%@'"
,字符 %
和 @
会被当做普通字符而失去了格式说明符的作用。
NSPredicate 字符串中也可以使用 %K
来指定键路径:
1 | predicate = [NSPredicate predicateWithFormat:@"%K == %@", @"name", @"Herbie"]; |
为了构造灵活的谓词,一种方式是使用格式说明符,另一种方式是将变量名放入字符串中,类似于环境变量:
1 | NSPredicate *predicateTemplate = [NSPredicate predicateWithFormat:@"name == $NAME"]; |
现在,我们有一个含有变量的谓词,可以使用 -predicateWithSubstitutionVariables:
来构造新的专用谓词,创建一个键/值对字典,其中键是变量名(不包含美元符号$),值是想要插入谓词的内容:
1 | NSDictionary *varDict = [NSDictionary dictionaryWithObjectsAndKeys:@"Herbie", @"NAME", nil]; |
这里使用字符串 “Herbie” 作为键 “NAME” 的值,构造以下新谓词:
1 | predicate = [predicateTemplate predicateWithSubstitutionVariables:varDict]; |
这个谓词跟之前的那些没什么不一样。
也可以用其他对象作为变量的值,例如 NSNumber,以下谓词用于过滤引擎的马力:
1 | predicateTemplate = [NSPredicate predicateWithFormat:@:"engine.horsepower > $POWER"]; |
除了使用 NSNumber 和 NSString 之外,也可以使用 [NSNull null]
来设置 nil 值,甚至可以使用数组。要注意的是,不能使用 "$变量名"
作为键路径,它只能表示值。使用谓词格式字符串时,如果想在程序中通过代码改变键路径,需要使用 %K
格式说明符。谓词机制不进行静态类型检查。
0x06 运算符
NSPredicate 的格式字符串包含大量不同的运算符。
比较和逻辑运算符
谓词字符串语法支持 C 语言中的一些常用运算符,不等号运算符如下表所示:
运算符 | 比较作用 |
---|
| 大于某数
= 和 => | 大于或等于某数
< | 小于某数
<= 和 =< | 小于或等于某数
!= 和 <> | 不等于某数
此外,谓词字符串语法还支持括号表达式、AND、OR 和 NOT 逻辑运算符,以及用 C 语言样式表示具有相同功能的 “&&”、”||” 和 “!” 符号。
我们可以筛选出马力在某个范围内的汽车:
1 | predicate = [NSPredicate predicateWithFormat:@"(engine.horsepower > 50) AND (engine.horsepower < 200)"]; |
上面是筛选出马力在50和200之间的汽车。
谓词字符串中的运算符不区分大小写,比如上面的 AND,可以随便写,比如:And、aNd等。
不等号既适用于数字值也适用于字符串值,比如按照字母表顺序查看所有名字排在 “Newton” 之前的汽车:
1 | predicate = [NSPredicate predicateWithFormat:@"name < 'Newton'"]; |
数组运算符
上面筛选马力在50到200之间的汽车使用的谓词字符串为 "(engine.horsepower > 50) AND (engine.horsepower < 200)"
,我们还可以写得更加简洁:
1 | predicate = [NSPredicate predicateWithFormat:@"engine.horsepower BETWEEN { 50, 200 }"]; |
花括号表示数组,BETWEEN 将数组中的第一个元素看成数组的下限,第二个元素看成数组的上限。
可以使用 %@
格式说明符向 NSArray 数组中插入对象:
1 | NSArray *betweens = [NSArray arrayWithObjects:[NSNumber numberWithInt:50], [NSNumber numberWithInt:200], nil]; |
也可以使用变量:
1 | predicateTemplate = [NSPredicate predicateWithFormat:@"engine.horsepower BETWEEN $POWERS"]; |
数组不仅可以用来指定某个区间的端点值,还可以配合 IN 运算符来查找数组中是否含有某个特定值,跟 SQL 很像哈:
1 | predicate = [NSPredicate predicateWithFormat:@"name IN { 'Herbie', 'Snugs', 'Badger', 'Flap' }"]; |
0x07 有 SELF 就足够了
有时候,我们可能需要将谓词应用于简单的值(例如纯文本的字符串),而不是那些通过键路径进行操作的复杂对象,比如我们需要从一汽车名称的数组中查询 name 时,就不能像之前那样子用了,这时候就轮到 SELF
出场了!
SELF
表示的是响应谓词计算的对象,事实上我们可以将谓词中所有的键路径表示成对应的 SELF 形式:
1 | predicate = [NSPredicate predicateWithFormat:@"SELF.name IN { 'Herbie', 'Snugs', 'Badger', 'Flap' }"]; |
我们先获取所有的汽车名称,然后构造一个谓词,并计算该谓词的值:
1 | names = [cars valueForKey:@"name"]; |
结果和前面的示例是一样的:Herbie 和 Badger。
这里提一个问题,以下代码会输出什么呢?
1 | NSArray *names1 = [NSArray arrayWithObjects:@"Herbie", @"Badger", @"Judge", @"Elvis", nil]; |
答案如下:
1 | ( |
对于取两个数组交集的运算来说,这种方式很巧妙。谓词包含了第一个数组的内容,因此和下面的形式类似:
1 | SELF IN {"Herbie", "Badger", "Judge", "Elvis"} |
现在,使用该谓词筛选第二个名称数组,在 name2 中如果有同时存在两个数组中的字符串,那么 SELF IN
会确定它是符合条件的,因此它就会保留在结果数组中,如果对象只存在于第二个数组中,那么它不会与谓词中的任何字符串匹配,所以该对象将被过滤掉,而只存在于第一个数组中的字符串因为要用来进行比较,所以将一直保留在原来的位置,不会出现在结果数组中。
0x08 字符串运算符
之前使用字符串时说到过关系运算符,此外,还有一些针对字符串的关系运算符:
运算符 | 意义 |
---|---|
BEGINSWITH | 检查某个字符串是否以另一个字符串开头 |
ENDSWITH | 检查某个字符串是否以另一个字符串结尾 |
CONTAINS | 检查某个字符串是否在另一个字符串内部 |
使用关系运算符可以执行一些有用的操作,例如使用 "name BEGINSWITH 'Bad'"
匹配 Badger,使用 "name ENDSWITH 'vis'"
匹配 Elvis,以及使用 "name CONTAINS udg"
匹配 Judge。
如果是 "name BEGINSWITH 'HERB'"
这样的呢?它不会与 Herbie 匹配,因为这些匹配是区分大小写的,同样,"name BEGINSWITH 'Hérb'"
也不会与之匹配,因为其中的 e 是含有重音符的。为了避免这些情况,可以为运算符添加 [c]、[d] 或 [cd] 修饰符,其中 c 表示 “不区分大小写”,d 表示 “不区分发音符号(diacritic insensitive,即忽略重音符)”,cd 表示 “即不区分大小写,也不区分发音符号”。通常情况下,都会使用这两个修饰符,除非有特殊需求需要区分大小写或者重音符号。
所以 Herbie 和 “name BEGINSWITH[cd] ‘HERB’” 相匹配。
0x09 LIKE 运算符
了解 SQL 的同学看到 LIKE 应该能猜到了,没错,有时候匹配开头或者结尾,又或者是否包含还不够,所以谓词格式字符串还提供了 LIKE 运算符,问号表示与一个字符匹配,型号表示与任意个字符匹配,也可以称为 “通配符”。
谓词字符串 “name LIKE ‘*er*‘“ 将会与任何含有 er 的名称相匹配,等效于 CONTAINS。
谓词字符串 “name LIKE ‘???er*’” 将会与 Pager Car 相匹配,因为其中的 er 前面有3个字符,后面也有一些字符。但是它与 Badger 不匹配,因为 Badger 的 er 前面有4个字符。
另外,LIKE 也接收 [cd] 修饰符,用户忽略大小写和发音符号。
如果你喜欢用正则表达式,可以使用 MATCHES 运算符,赋给它一个正则表达式,谓词将会计算出它的值。