當向支持KVC的對象發(fā)送valueForKeyPath:消息時,你可以在key path中嵌入一個 Collection Operators(集合運算符)。 Collection Operators是一個以at符號(@)開頭的關(guān)鍵字,它指定了一個操作: getter應(yīng)該在返回之前以某種方式對數(shù)據(jù)進行操作。NSObject的valueForKeyPath:方法的默認實現(xiàn)已經(jīng)實現(xiàn)了這種行為。
當key path包含集合運算符時,運算符之前的key path的部分被稱為left key path, 表示 receiver在其上操作的集合。如果直接向集合對象(比如NSArray)發(fā)送消息,left key path可能會被忽略。
在運算符后面的key path部分稱為right key path,指定了運算符應(yīng)該處理的集合中的屬性。 所有的集合運算符(除了@count)都需要有right key path。下圖說明了運算符的key path格式。

集合運算符含有三種基本類型:
Aggregation Operators(聚合運算符): 以某種方式合并集合中的對象,并返回一個通常與
right key path指定的屬性的數(shù)據(jù)類型相匹配的對象。(@count運算符除外,它不需要right key path,并且始終返回一個NSNumber實例)Array Operators(數(shù)組運算符):返回一個數(shù)組實例,該實例包含指定集合中的一些對象子集。
Nesting Operators(嵌套運算符):處理包含其他集合的集合(就是一個嵌套
array或者嵌套set),并返回一個NSArray或者NSSet實例,具體取決于運算符,它以某種方式組合了嵌套集合的屬性。

看到這些還是不太懂運算符的作用,別慌,通過幾個例子來看一下各個運算符的作用
樣本數(shù)據(jù)
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數(shù)組,該數(shù)組存放的是Transaction對象,假設(shè)該數(shù)組填充了Table 4-1的數(shù)據(jù)
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屬性,產(chǎn)生一個反映集合某些方面的單個值。
@avg
當你指定@avg運算符時,valueForKeyPath:會為集合中每個元素讀取由right key path指定的屬性,將其轉(zhuǎn)化為double類型(將nil轉(zhuǎn)為0),并計算這些元素的算術(shù)平均值。并返回結(jié)果存儲在NSNumber的實例
獲取表4-1中樣本數(shù)據(jù)的平均交易額
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
上面的方法中,
valueForKeyPath:會讀取集合(這里是transactions數(shù)組)中每個元素(這里是Transaction對象)中right key path指定的屬性(這里是amount),將該屬性的value轉(zhuǎn)為double類型,并計算它們的算術(shù)平均數(shù),將平均數(shù)轉(zhuǎn)為NSNumber返回transactionAverage的結(jié)果是
456.54
@count
當你指定@count運算符時,valueForKeyPath:會返回集合中對象的數(shù)量(NSNumber實例),right key path(如果存在)會被忽略
獲取transactions中Transaction對象的數(shù)量
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
numberOfTransactions的值是13;
可以看到@count是不需要right key path的
@max
當你指定@max運算符,valueForKeyPath:將在由right key path指定的集合條目中進行搜索并返回最大值。搜索使用compare:方法進行比較。因此,由right key path指定的屬性必須能夠?qū)υ摲椒ㄟM行有效的響應(yīng)。該搜索忽略集合中的nil值。
進行以下操作獲取表4-1中transactions中的最大日期值
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
latestDate的值是 Jul 15, 2016.注意:
right key path指定的屬性必須能響應(yīng)compare:方法,否則會拋出NSInvalidArgumentException異常比如在
Transaction中添加一個owner屬性,owner是一個Person對象(繼承自NSObject),不能響應(yīng)compare:方法。調(diào)用下面方法
[self.transactions valueForKeyPath:@"@max.owner"];就會拋出異常:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person compare:]: unrecognized selector sent to instance 0x604000002750'
從reason中也可以看出
valueForKeyPath:方法確實是調(diào)用的compare :
@min
當你指定@min運算符的時候,valueForKeyPath:將在由right key path指定的集合條目中進行查詢并返回最小值。該查詢使用compare:方法進行比較。因此,right key path指定的屬性必須能夠?qū)Υ朔椒ㄟM行有意義響應(yīng)。搜索將忽略集合中的nil值。(跟@max很相似)
獲取表4-1列出的交易中日期值(最早交易日期)的最小值:
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
earliestDate的值是Dec 1, 2015.
@sum
當你指定@sum運算符時,valueForKeyPath:為集合中的每一個元素讀取由right key path指定的屬性,并轉(zhuǎn)為double類型(nil值用0替代),并計算它們的和。并返回該結(jié)果(將返回值存儲在NSNumber中返回)
獲取表4-1中樣本數(shù)據(jù)中交易金額的總和:
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時會發(fā)生什么?
right key path指定的對象是要進行Operators所指定的操作的,以@max為例,當執(zhí)行下面的方法時,
[self.transactions valueForKeyPath:@"@max.self"]
通過上面我們知道right key path指定的屬性對象會調(diào)用compare:方法,而現(xiàn)在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"]
因為數(shù)組中存放的是NSNumber,當right key path為self時,是數(shù)組中的NSNumber進行比較,最后返回最大值@3。 其他的運算符類似。
Array Operators(數(shù)組運算符)
數(shù)組運算符使valueForKeyPath:返回對應(yīng)于right key path指定的一組特定對象的對象數(shù)組。 即數(shù)組運算符返回的是一個數(shù)組,數(shù)組中存放的是right key path指定的對象屬性。
注意:當使用數(shù)組運算符時,如果
leaf objects(葉子對象)為nil,則valueForKeyPath:方法會拋出異常
@distinctUnionOfObjects
當你指定了@distinctUnionOfObjects運算符,valueForKeyPath:創(chuàng)建并返回一個數(shù)組,該數(shù)組包含與right key path指定的屬性相對應(yīng)的集合中不同對象??梢岳斫鉃閿?shù)組中right key path指定的屬性組成一個新的數(shù)組,并對該數(shù)組中重復(fù)的數(shù)據(jù)進行移除,最后返回該數(shù)組。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
獲得一個
payee屬性值的數(shù)組集合,transactions數(shù)組中的Transaction對象的payee屬性的值如果有重復(fù)的將會被忽略。可以理解為將
transactions數(shù)組中的所有Transaction對象的payee屬性值存放到一個新的數(shù)組中,將新數(shù)組中重復(fù)的值移除并返回該數(shù)組。
distinctPayees數(shù)組包含以下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage.注意:
@unionOfObjects運算符的作用與其類似,但是沒有進行去重。
@unionOfObjects
當你指定@unionOfObjects運算符時,valueForKeyPath:創(chuàng)建并返回一個數(shù)組,該數(shù)組包含所有的集合(right key path指定的屬性所組成的集合)對象。跟@ distinctUnionOfObjects非常類似,區(qū)別就是重復(fù)的對象不會移除。
獲取在transactions中的Transaction對象的 payee屬性的值的集合
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
payees數(shù)組包含以下字符串: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數(shù)組運算符返回的結(jié)果是數(shù)組,該數(shù)組存放的數(shù)據(jù)是right key path指定的屬性。
當
right key path為self時,其效果就是把數(shù)組中的數(shù)據(jù)進行去重處理(distinct)或者不去重。-
因為
@distinctUnionOfObjects會對數(shù)據(jù)進行去重,所以新返回的數(shù)組里的數(shù)據(jù)的前后順序跟原來數(shù)組的順序可能會不一樣,而@unionOfObjects不去重,所以返回的數(shù)據(jù)跟原來數(shù)組里的數(shù)據(jù)順序一致。比如:
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,跟數(shù)組中原有的順序一致。
前面說過,當使用數(shù)組運算符時,如果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數(shù)組中的數(shù)據(jù):
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:創(chuàng)建并返回一個數(shù)組,該數(shù)組包含與right key path指定的屬性對應(yīng)的所有集合的組合的不同對象。與數(shù)組運算符@distinctUnionOfObjects很相似。
在arrayOfArrays中的數(shù)組獲取屬性payee的不同的值。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
collectedDistinctPayees數(shù)組包含以下值:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power.
@unionOfArrays運算符與該運算符作用相似,只不過沒有進行去重
@unionOfArrays
當指定@unionOfArrays運算符時,valueForKeyPath:創(chuàng)建并返回一個數(shù)組,
該數(shù)組包含所有的right key path指定的屬性。與@distinctUnionOfArrays類似,只不過不進行去重。
獲取arrayOfArrays數(shù)組中所有的對象的payee屬性的值。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
collectedPayees數(shù)組的包含的數(shù)據(jù)有: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:創(chuàng)建并返回一個NSSet對象,該對象包含right key path指定的屬性組成的集合的所有不同對象。
這個運算符跟@distinctUnionOfArrays很類似,只不過它作用于包含NSSet的NSSet,而不是包含數(shù)組的數(shù)組, 并且返回的是NSSet而不是數(shù)組。
另外,想當然的會聯(lián)想到既然有了@distinctUnionOfSets運算符,是不是跟上面的一樣會有@unionOfSets運算符? 對于NSSet來說是沒有@unionOfSets運算符的,如果使用的話會拋出'NSInvalidArgumentException'異常,提示NSSet沒有實現(xiàn)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的數(shù)組屬性,我們也可以這樣
NSNumber *avg = [self valueForKeyPath:@"transactions.@avg.amount"];
兩者是等效的,當然left key path既然是一個key path,當然也可以這樣
NSNumber *avg = [self valueForKeyPath:@"owner.numbers.@avg.self"];
這里的owner是Person實例,numbers是Person的一個數(shù)組,里面存放的是@1,@3,@5,由于這里存放的是NSNumber實例,所以right key path寫成self。運行的是結(jié)果為
3
自定義集合運算符
上面介紹了一些運算符的用法,我們很自然的想要自定義運算符,不過我翻了一下蘋果文檔,并沒有找到關(guān)于自定義集合運算符的相關(guān)介紹,倒是在NSHipster看到Mattt 大神于2012年的一篇文章中提到蘋果文檔曾這么說過:
Note: It is not currently possible to define your own collection operators.
雖然蘋果沒有提供自定義集合運算符的文檔,但是這篇文章還是提供了一些思路:
swizzles
valueForKeyPath:來解析自定義的DSL,來實現(xiàn)自定義的效果。
不過我在Github上找到了一個關(guān)于自定義集合運算符的demo,里面自定義了諸如unionOfPresentObjects(跟@unionOfObjects類似,但忽略 不兼容/ nil / NSNull 的對象),@standardDeviation,@"@reverse"等自定義運算符,感興趣的可以點擊這里到Github查看
根據(jù)這個demo發(fā)現(xiàn),我們?yōu)橐粋€數(shù)組添加一個<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
然后我們就可以在我們自定義的方法里做一些我們想要的操作。
參考