KVC之使用Collection Operators(集合运算符)

当向支持KVC的对象发送valueForKeyPath:消息时,你可以在key path中嵌入一个 Collection Operators(集合运算符)Collection Operators是一个以at符号(@)开头的关键字,它指定了一个操作: getter应该在返回之前以某种方式对数据进行操作。NSObjectvalueForKeyPath:方法的默认实现已经实现了这种行为。

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(如果存在)会被忽略

获取transactionsTransaction对象的数量

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 pathself时会发生什么?

right key path指定的对象是要进行Operators所指定的操作的,以@max为例,当执行下面的方法时,

[self.transactions valueForKeyPath:@"@max.self"]

通过上面我们知道right key path指定的属性对象会调用compare:方法,而现在right key pathself,也就是指定的是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 pathself时,是数组中的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 pathself时,其效果就是把数组中的数据进行去重处理(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很类似,只不过它作用于包含NSSetNSSet,而不是包含数组的数组, 并且返回的是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类的实例,transactionsBankAccount的数组属性,我们也可以这样

NSNumber *avg = [self valueForKeyPath:@"transactions.@avg.amount"];

两者是等效的,当然left key path既然是一个key path,当然也可以这样

 NSNumber *avg = [self valueForKeyPath:@"owner.numbers.@avg.self"];

这里的ownerPerson实例,numbersPerson的一个数组,里面存放的是@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

然后我们就可以在我们自定义的方法里做一些我们想要的操作。

参考

推荐阅读更多精彩内容