当向支持KVC的对象发送valueForKeyPath:
消息时,你可以在key path
中嵌入一个 Collection Operators(集合运算符)
。 Collection Operators
是一个以at符号(@)开头的关键字,它指定了一个操作: getter应该在返回之前以某种方式对数据进行操作。NSObject
的valueForKeyPath:
方法的默认实现已经实现了这种行为。
当key path
包含集合运算符时,运算符之前的key path
的部分被称为left key path
, 表示 receiver
在其上操作的集合。如果直接向集合对象(比如NSArray
)发送消息,left key path
可能会被忽略。
在运算符后面的key path
部分称为right key path
,指定了运算符应该处理的集合中的属性。 所有的集合运算符(除了@count
)都需要有right key path
。下图说明了运算符的key path
格式。
集合运算符含有三种基本类型:
Aggregation Operators(聚合运算符): 以某种方式合并集合中的对象,并返回一个通常与
right key path
指定的属性的数据类型相匹配的对象。(@count
运算符除外,它不需要right key path
,并且始终返回一个NSNumber
实例)Array Operators(数组运算符):返回一个数组实例,该实例包含指定集合中的一些对象子集。
Nesting Operators(嵌套运算符):处理包含其他集合的集合(就是一个嵌套
array
或者嵌套set
),并返回一个NSArray
或者NSSet
实例,具体取决于运算符,它以某种方式组合了嵌套集合的属性。
看到这些还是不太懂运算符的作用,别慌,通过几个例子来看一下各个运算符的作用
样本数据
Listing 4-1:
@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance; // An attribute
@property (nonatomic) Person* owner; // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end
@interface Transaction : NSObject
@property (nonatomic) NSString* payee; // To whom
@property (nonatomic) NSNumber* amount; // How much
@property (nonatomic) NSDate* date; // When
@end
BankAccount
实例有一个transactions
数组,该数组存放的是Transaction
对象,假设该数组填充了Table 4-1的数据
Table: 4-1:
payee values |
amount values |
date values |
---|---|---|
Green Power | $120.00 | Dec 1, 2015 |
Green Power | $150.00 | Jan 1, 2016 |
Green Power | $170.00 | Feb 1, 2016 |
Car Loan | $250.00 | Jan 15, 2016 |
Car Loan | $250.00 | Feb 15, 2016 |
Car Loan | $250.00 | Mar 15, 2016 |
General Cable | $120.00 | Dec 1, 2015 |
General Cable | $155.00 | Jan 1, 2016 |
General Cable | $120.00 | Feb 1, 2016 |
Mortgage | $1,250.00 | Jan 15, 2016 |
Mortgage | $1,250.00 | Feb 15, 2016 |
Mortgage | $1,250.00 | Mar 15, 2016 |
Animal Hospital | $600.00 | Jul 15, 2016 |
Aggregation Operators (聚合运算符)
Aggregation operators(聚合运算符)可以处理array
或者set
属性,产生一个反映集合某些方面的单个值。
@avg
当你指定@avg
运算符时,valueForKeyPath:
会为集合中每个元素读取由right key path
指定的属性,将其转化为double
类型(将nil转为0),并计算这些元素的算术平均值。并返回结果存储在NSNumber
的实例
获取表4-1中样本数据的平均交易额
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
上面的方法中,
valueForKeyPath:
会读取集合(这里是transactions
数组)中每个元素(这里是Transaction
对象)中right key path
指定的属性(这里是amount
),将该属性的value
转为double
类型,并计算它们的算术平均数,将平均数转为NSNumber返回transactionAverage的结果是
456.54
@count
当你指定@count
运算符时,valueForKeyPath:
会返回集合中对象的数量(NSNumber
实例),right key path
(如果存在)会被忽略
获取transactions
中Transaction
对象的数量
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
numberOfTransactions
的值是13;
可以看到@count
是不需要right key path
的
@max
当你指定@max
运算符,valueForKeyPath:
将在由right key path
指定的集合条目中进行搜索并返回最大值。搜索使用compare:
方法进行比较。因此,由right key path
指定的属性必须能够对该方法进行有效的响应。该搜索忽略集合中的nil
值。
进行以下操作获取表4-1中transactions
中的最大日期值
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
latestDate
的值是 Jul 15, 2016.注意:
right key path
指定的属性必须能响应compare:
方法,否则会抛出NSInvalidArgumentException
异常比如在
Transaction
中添加一个owner
属性,owner
是一个Person
对象(继承自NSObject
),不能响应compare:
方法。调用下面方法
[self.transactions valueForKeyPath:@"@max.owner"];
就会抛出异常:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person compare:]: unrecognized selector sent to instance 0x604000002750'
从reason中也可以看出
valueForKeyPath:
方法确实是调用的compare :
@min
当你指定@min
运算符的时候,valueForKeyPath:
将在由right key path
指定的集合条目中进行查询并返回最小值。该查询使用compare:
方法进行比较。因此,right key path
指定的属性必须能够对此方法进行有意义响应。搜索将忽略集合中的nil值。(跟@max
很相似)
获取表4-1列出的交易中日期值(最早交易日期)的最小值:
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
earliestDate
的值是Dec 1, 2015.
@sum
当你指定@sum
运算符时,valueForKeyPath:
为集合中的每一个元素读取由right key path
指定的属性,并转为double类型(nil值用0替代),并计算它们的和。并返回该结果(将返回值存储在NSNumber
中返回)
获取表4-1中样本数据中交易金额的总和:
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
amountSum
的值是5,935.00
。
提示:当集合中存放的是
NSNumber
时,可以直接把self
作为right key path
,比如[@[@(1), @(2), @(3)] valueForKeyPath:@"@max.self"]
返回的是
@3
想一下,当right key path
是self
时会发生什么?
right key path
指定的对象是要进行Operators
所指定的操作的,以@max
为例,当执行下面的方法时,
[self.transactions valueForKeyPath:@"@max.self"]
通过上面我们知道right key path
指定的属性对象会调用compare:
方法,而现在right key path
为self
,也就是指定的是Transaction
实例对象,而Transaction
是没有compare:
方法的,所以会抛出错误:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Transaction compare:]: unrecognized selector sent to instance 0x600000252e40'
但如果是下面这样就不会出错了
[@[@(1), @(2), @(3)] valueForKeyPath:@"@max.self"]
因为数组中存放的是NSNumber
,当right key path
为self
时,是数组中的NSNumber
进行比较,最后返回最大值@3
。 其他的运算符类似。
Array Operators(数组运算符)
数组运算符使valueForKeyPath:
返回对应于right key path
指定的一组特定对象的对象数组。 即数组运算符返回的是一个数组,数组中存放的是right key path
指定的对象属性。
注意:当使用数组运算符时,如果
leaf objects
(叶子对象)为nil,则valueForKeyPath:
方法会抛出异常
@distinctUnionOfObjects
当你指定了@distinctUnionOfObjects
运算符,valueForKeyPath:
创建并返回一个数组,该数组包含与right key path
指定的属性相对应的集合中不同对象。可以理解为数组中right key path
指定的属性组成一个新的数组,并对该数组中重复的数据进行移除,最后返回该数组。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
获得一个
payee
属性值的数组集合,transactions
数组中的Transaction
对象的payee
属性的值如果有重复的将会被忽略。可以理解为将
transactions
数组中的所有Transaction
对象的payee
属性值存放到一个新的数组中,将新数组中重复的值移除并返回该数组。
distinctPayees
数组包含以下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage
.注意:
@unionOfObjects
运算符的作用与其类似,但是没有进行去重。
@unionOfObjects
当你指定@unionOfObjects
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含所有的集合(right key path
指定的属性所组成的集合)对象。跟@ distinctUnionOfObjects
非常类似,区别就是重复的对象不会移除。
获取在transactions
中的Transaction
对象的 payee
属性的值的集合
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
payees
数组包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital
.
从上面的例子中可以看出Array Operators
数组运算符返回的结果是数组,该数组存放的数据是right key path
指定的属性。
当
right key path
为self
时,其效果就是把数组中的数据进行去重处理(distinct
)或者不去重。-
因为
@distinctUnionOfObjects
会对数据进行去重,所以新返回的数组里的数据的前后顺序跟原来数组的顺序可能会不一样,而@unionOfObjects
不去重,所以返回的数据跟原来数组里的数据顺序一致。比如:
NSArray *array = @[@"blue",@"blue",@"blue",@"blue",@"red",@"yellow",@"purple"]; NSArray *result = [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
打印为: yellow, purple, blue, red, 而不是blue, red, yellow, purple,
NSArray *result = [array valueForKeyPath:@"@unionOfObjects.self"];
这里打印 blue, blue, blue, red, yellow,purple,跟数组中原有的顺序一致。
前面说过,当使用数组运算符时,如果leaf objects
(叶子对象)为nil,则valueForKeyPath:方法会抛出异常
比如
id objects = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"green"},
@"notacolor"];
[objects valueForKeyPath:@"@unionOfObjects.color"];
则会抛出异常
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFConstantString 0x10aa4d470> valueForUndefinedKey:]: this class is not key value coding-compliant for the key
Nesting Operators(嵌套运算符)
嵌套运算符对嵌套集合(集合中的每个条目本身又包含了一个集合)进行操作,
当使用嵌套运算符时,如果
leaf objects (叶子对象)
为nil
,valueForKeyPath:
方法会抛出一个异常。
NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
moreTransactions
数组中的数据:
payee values |
amount values |
date values |
---|---|---|
General Cable - Cottage | $120.00 | Dec 18, 2015 |
General Cable - Cottage | $155.00 | Jan 9, 2016 |
General Cable - Cottage | $120.00 | Dec 1, 2016 |
Second Mortgage | $1,250.00 | Nov 15, 2016 |
Second Mortgage | $1,250.00 | Sep 20, 2016 |
Second Mortgage | $1,250.00 | Feb 12, 2016 |
Hobby Shop | $600.00 | Jun 14, 2016 |
@distinctUnionOfArrays
当你指定@distinctUnionOfArrays
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与right key path
指定的属性对应的所有集合的组合的不同对象。与数组运算符@distinctUnionOfObjects
很相似。
在arrayOfArrays
中的数组获取属性payee
的不同的值。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
collectedDistinctPayees
数组包含以下值:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power
.
@unionOfArrays
运算符与该运算符作用相似,只不过没有进行去重
@unionOfArrays
当指定@unionOfArrays
运算符时,valueForKeyPath:
创建并返回一个数组,
该数组包含所有的right key path
指定的属性。与@distinctUnionOfArrays
类似,只不过不进行去重。
获取arrayOfArrays
数组中所有的对象的payee
属性的值。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
collectedPayees
数组的包含的数据有:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop.
@distinctUnionOfSets
当指定@distinctUnionOfSets
运算符时,valueForKeyPath:
创建并返回一个NSSet
对象,该对象包含right key path
指定的属性组成的集合的所有不同对象。
这个运算符跟@distinctUnionOfArrays
很类似,只不过它作用于包含NSSet
的NSSet
,而不是包含数组的数组, 并且返回的是NSSet
而不是数组。
另外,想当然的会联想到既然有了@distinctUnionOfSets
运算符,是不是跟上面的一样会有@unionOfSets
运算符? 对于NSSet
来说是没有@unionOfSets
运算符的,如果使用的话会抛出'NSInvalidArgumentException'异常,提示NSSet
没有实现unionOfSets
运算符。
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<__NSSetI 0x60400025c530> valueForKeyPath:]: this class does not implement the unionOfSets operation.
left key path
前面对left key path
没有细讲,这里对其简单介绍下,大致如下:
[object valueForKeyPath: @"keypathToCollection.@collectionOperator.keypathToProperty"]
等效于
[[object valueForKeyPath:@"keyPathToCollection"]
valueForKeyPath:@"@collectionOperator.keypathToProperty"]
也等效于
[[[object valueForKeyPath:@"keyPathToCollection"]
valueForKeyPath:@"keypathToProperty"]
performTheCollectionOperator]
举个例子,正如前面说到的@avg
运算符,
NSNumber *avg = [self.transactions valueForKeyPath:@"@avg.amount"];
这里的self
是指BankAccount
类的实例,transactions
是BankAccount
的数组属性,我们也可以这样
NSNumber *avg = [self valueForKeyPath:@"transactions.@avg.amount"];
两者是等效的,当然left key path
既然是一个key path
,当然也可以这样
NSNumber *avg = [self valueForKeyPath:@"owner.numbers.@avg.self"];
这里的owner
是Person
实例,numbers
是Person
的一个数组,里面存放的是@1,@3,@5
,由于这里存放的是NSNumber
实例,所以right key path
写成self
。运行的是结果为
3
自定义集合运算符
上面介绍了一些运算符的用法,我们很自然的想要自定义运算符,不过我翻了一下苹果文档,并没有找到关于自定义集合运算符的相关介绍,倒是在NSHipster看到Mattt 大神于2012年的一篇文章中提到苹果文档曾这么说过:
Note: It is not currently possible to define your own collection operators.
虽然苹果没有提供自定义集合运算符的文档,但是这篇文章还是提供了一些思路:
swizzles
valueForKeyPath:
来解析自定义的DSL,来实现自定义的效果。
不过我在Github上找到了一个关于自定义集合运算符的demo,里面自定义了诸如unionOfPresentObjects
(跟@unionOfObjects
类似,但忽略 不兼容/ nil / NSNull 的对象),@standardDeviation
,@"@reverse"
等自定义运算符,感兴趣的可以点击这里到Github查看
根据这个demo发现,我们为一个数组添加一个<collectionOperator>ForKeyPath:
或_<collectionOperator>ForKeyPath:
的方法,然后我们就可以通过像使用集合运算符的方法使用该方法,比如:
@interface NSArray (CollectionOperator)
- (id)myCustomOperatorForKeyPath:(NSString *)keyPath;
@end
@implementation NSArray (CollectionOperator)
- (id)myCustomOperatorForKeyPath:(NSString *)keyPath
{
NSLog(@"myCustomOperatorForKeyPath : %@", keyPath);
return nil;
}
@end
使用
[self.positions valueForKeyPath:@"@myCustomOperator.self"];
打印信息为:
myCustomOperatorForKeyPath : self
然后我们就可以在我们自定义的方法里做一些我们想要的操作。
参考