KVC詳解

關於KVC

KVC爲遵守NSKeyValueCoding協議的對象提供間接的方式來訪問它們的屬性。當對象符合KVC,屬性能通過字符串來進行訪問,也可以通過實例變量和對應的訪問方法訪問屬性。

獲取訪問方法返回屬性的值,設置訪問方法設置屬性的值。在OC,你可以使用實例變量訪問屬性。雖然這些方式可以直接訪問屬性,但是需要使用特定的訪問方法和實例變量。相反,KVC對象提供簡單的方法來統一訪問所有屬性。

KVC是構成其他cocoa技術的基本概念,例如,KVO,cocoa-binding,Core Data,AppleScript-ability。KVC也會簡化你的代碼。

使用KVC對象

當對象繼承NSObject,它就實現了KVC。你可以實現下面的任務:

• 訪問對象屬性。

• 操作集合屬性。

• 使用集合操作符。

• 訪問非對象屬性。

• 通過鍵路徑訪問屬性。

爲對象符合KVC要求

繼承NSObject的對象默認實現KVC。爲了讓KVC更好的實現,你需要確保訪問方法和實例變量的聲明符合命名規則。你也可以擴展和自定義KVC方法。


訪問對象屬性

對象的屬性(property)可以聲明在類的接口聲明和種類的接口聲明。

• 屬性(attribute)。它們是簡單的值,例如,數值,字符串,布爾值。值對象NSNumber。

• 一對一關係。對象屬性。

• 一對多關係。集合屬性。

@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

使用鍵和鍵路徑來標記對象屬性

鍵一般使用屬性的名稱,但不能有空格,通常小寫開頭。鍵路徑使用點劃分的多鍵字符串來表達要訪問對象屬性的順序路徑。第一個鍵是對應訪問對象屬性的值,接下來的鍵對應訪問這個屬性值的屬性值。

使用鍵獲取屬性的值

• valueForKey: - 返回鍵名對應的值。如果提供的鍵名不能通過訪問搜索模式獲取值,對象會調用valueForUndefinedKey:方法。這個方法默認會拋出一個NSUndefinedKeyException異常,你可以在子類重寫這個方法來處理這種情況。

• valueForKeyPath: - 返回鍵路徑對應的值。如果在鍵路徑序列中,某個鍵不是對應一個KVC對象,那麼valueForKey: 不能找到訪問方法,之後對象調用valueForUndefinedKey:方法。

• dictionaryWithValuesForKeys: - 返回一組鍵對應的NSDictionary對象,這個方法會依次調用valueForKey:方法來獲取對應鍵的值。這個字典包含所有的鍵和值。

注意:由於不可變的集合是不能存儲nil值,所以KVC會自動地把nil值轉換爲NSNull對象。

當你使用鍵路徑來標記屬性,如果最後的鍵對應訪問一對多關係(對象集合)的屬性值,返回的值是一個包含對象集合的每個對象屬性值的集合。

使用鍵設置屬性的值

• setValue:forKey: - 設置鍵名對應屬性的值。默認會自動解包裝NSNumber和NSValue爲指定的值類型。如果指定的鍵名沒有找到設置方法,對象會調用setValue:forUndefinedKey:方法。這個方法默認會拋出一個NSUndefinedKeyException異常,你可以在子類重寫這個方法來處理這種情況。

• setValue:forKeyPath: - 設置鍵路徑對應屬性的值。如果在鍵路徑序列中,某個鍵不是對應一個KVC對象,那麼對象會對象調用valueForUndefinedKey:方法。

• setValuesForKeysWithDictionary: - 使用NSDictionary設置多個鍵對應屬性的值。默認會依次調用setValue:forKey:方法,對NSNULL對象會解析成nil。

在系統默認實現中,設置不是對象屬性的值爲nil時,對象會調用setNilValueForKey:方法。這個方法默認會拋出一個NSInvalidArgumentException異常,你可以在子類重寫這個方法來處理這種情況。

使用鍵來簡化對象訪問

不用KVC:

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    id result = nil;
    Person *person = [self.people objectAtIndex:row];
 
    if ([[column identifier] isEqualToString:@"name"]) {
        result = [person name];
    } else if ([[column identifier] isEqualToString:@"age"]) {
        result = @([person age]);  // Wrap age, a scalar, as an NSNumber
    } else if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        result = [person favoriteColor];
    } // And so on...
 
    return result;
}

使用KVC:

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}


訪問集合屬性

一對多關係屬性和其他屬性一樣,可以使用其他屬性的KVC訪問方法(valueForKey: ,setValue:forKey:)。當你操作集合內容時,使用KVC可變集合代理方法會更有效。

• mutableArrayValueForKey: 和mutableArrayValueForKeyPath: - 返回一個NSMutableArray代理對象(不是NSMutableArray,但可以像NSMutableArray一樣操作)。

• mutableSetValueForKey: 和mutableSetValueForKeyPath: - 返回一個NSMutableSet代理對象(不是NSMutableSet,但可以像NSMutableSet一樣操作)。

• mutableOrderedSetValueForKey: 和mutableOrderedSetValueForKeyPath: - 返回一個NSMutableOrderedSet代理對象(不是NSMutableOrderedSet,但可以像NSMutableOrderedSet一樣操作)。

當你操作代理對象,添加對象,刪除對象或者替換對象,代理對象默認會修改對應的屬性值。這可能比valueForKey:返回的不可變集合更有效,valueForKey:這種方式會再創建一個已修改的集合並重新調用setValue:forKey:方法來設置屬性值。這可能比直接訪問可變集合屬性更有效。這些KVC可變集合代理方法會提供額外的好處去進行KVO的集合屬性操作。

使用集合操作符

當你對KVC對象使用valueForKeyPath:方法時,你可以嵌入集合操作符在鍵路徑。集合操作符是一個帶有@前綴的關鍵字,它會在返回數據前進行一些操作。

當鍵路徑包含集合操作符,操作符左邊的鍵路徑爲左鍵路徑,指示要對左鍵路徑的屬性值進行操作。如果你直接對集合對象使用集合操作符,例如NSArray實例,左操作符可以省略。


集合操作符有3種基本類型:

• 聚合操作符(Aggregation Operators)合併集合對象並返回一個單獨對象。這個對象會匹配右鍵路徑的屬性的數據類型。@count操作符除外,它沒有右鍵路徑而且總是返回NSNumber實例。

• 數組操作符(Array Operators)返回NSArray實例,它包含對應集合的子集對象。

• 嵌套操作符(Nesting Operators)操作嵌套集合(被集合包含的集合)並返回NSArray或者NSSet實例。返回的集合包含對象的屬性,這個對象被嵌套集合包含。

簡單數據

下面將會使用這些數據進行說明。

@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
聚合操作符

聚合操作符操作NSArray或者NSSet集合屬性,返回一個單獨的值來反映聚合結果。

@avg

當你使用@avg操作符,valueForKeyPath:會依次讀取每一個右鍵路徑的屬性值,轉化它們爲double值(nil值爲0)並計算平均值,最後返回平均值的NSNumber實例。

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

@count

當你使用@count操作符,valueForKeyPath:返回集合的對象數量,右操作符忽略。

NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];

@max

當你使用@max操作符,valueForKeyPath:會搜索右鍵路徑的屬性值的最大值。搜索使用compare:方法進行比較,大多數Foundation對象都有這個方法,例如NSNumber對象。右鍵路徑對應的對象必須有意義地響應這個方法。搜索會避免nil值。

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];

@min

當你使用@min操作符,valueForKeyPath:會搜索右鍵路徑的屬性值的最小值。搜索使用compare:方法進行比較,大多數Foundation對象都有這個方法,例如NSNumber對象。右鍵路徑對應的對象必須有意義地響應這個方法。搜索會避免nil值。

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

@sum

當你使用@sum操作符,valueForKeyPath:會依次讀取每一個右鍵路徑的屬性值,轉化它們爲double值(nil值爲0)並計算總和值,最後返回平均值的NSNumber實例。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
數組操作符

數組操作符會讓valueForKeyPath: 返回對應右鍵路徑對應屬性的數組集合的特殊子集合。

注意:如果使用數組操作符遇到葉子對象爲nil,valueForKeyPath:會拋出異常。

@distinctUnionOfObjects

當你使用@distinctUnionOfObjects操作符,valueForKeyPath:返回右鍵路徑對應屬性的一個包含不同對象的集合。

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

@unionOfObjects

當你使用@unionOfObjects操作符,valueForKeyPath:返回右鍵路徑對應屬性的一個包含所有對象的集合,它不會刪除重複對象。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
嵌套操作符

嵌套操作符操作嵌套集合,這個集合被集合包含。

注意:如果使用嵌套操作符遇到葉子對象爲nil,valueForKeyPath: 會拋出異常。

嵌套集合的例子:

NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];

@distinctUnionOfArrays

當你使用@distinctUnionOfArrays操作符,valueForKeyPath:返回右鍵路徑對應屬性的一個包含不同對象的集合(操作嵌套集合)。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

@unionOfArrays

當你使用@unionOfArrays操作符,valueForKeyPath:返回右鍵路徑對應屬性的一個包含所有對象的集合(操作嵌套集合),它不會刪除重複對象。

NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

@distinctUnionOfSets

當你使用@distinctUnionOfSets操作符,valueForKeyPath:返回右鍵路徑對應屬性的一個包含不同對象的集合(操作嵌套集合)。和@distinctUnionOfArrays不同的是嵌套集合是NSSet被NSSet包含而不是NSArray被NSArray包含,同樣它返回的是NSSet實例而不是NSArray實例。


代表非對象值

KVC對象會處理對象屬性和非對象屬性。默認會自動對對象參數或者返回值和非對象屬性之間進行轉換。這允許獲取方法和設置方法存儲的屬性爲結構體和數值。

當你執行KVC協議中的獲取方法時,例如valueForKey:,默認實現會根據訪問搜索模式和鍵來確定訪問方法和實例變量。如果返回值不是對象,獲取方法會使用返回值創建NSNumber(數值)或者NSValue(結構體)對象來代替返回值。

同樣的,setValue:forKey:方法會根據訪問方法或者實例變量和指定的鍵來決定數據類型。如果數據類型不是對象,設置方法首先會向設置的Value對象發送<type>Value方法來獲取底層數據並使用它來設置值。

注意:如果向KVC的設置方法提供nil值,會觸發setNilValueForKey:方法。默認這個方法會拋出NSInvalidArgumentException異常。你可以在子類重寫這個方法來處理這種情況。

包裝和解包裝數值類型

下面這些類型會使用NSNumber來包裝和解包裝。


包裝和解包裝結構體

下面這些類型會使用NSValue來包裝和解包裝。


如果不是NSPoint、NSRange、NSRect,NSSize的結構體,KVC也可以自動地使用NSValue來包裝和解包裝它(它們的類型編碼字符串必須以“{”開頭)。

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];


驗證屬性

KVC支持屬性驗證。就像你使用基於鍵來讀取和設置KVC對象屬性,你可以通過鍵或者鍵路徑驗證屬性。當你調用validateValue:forKey:error:或者validateValue:forKeyPath:error:方法,默認會搜索對象是否接受驗證方法(或者鍵路徑最後一個鍵),這個方法的命名符合validate<Key>:error:。如果對象沒有定義這個方法,默認驗證成功並返回YES。當特定屬性驗證方法存在,返回值由具體方法實現。

因爲指定屬性方法接收值對象和錯誤對象的引用,驗證有3種可能的輸出:

1. 認爲值對象是有效的並返回YES而且沒有修改值對象或者錯誤對象。

2. 認爲值對象是無效的並且沒有修改它。在這種情況下,返回NO並設置錯誤對象爲NSError對象實例。

3. 認爲值對象是無效的,但是創建新的有效值對象來代替它。在這種情況下,返回YES並且不改動錯誤對象。這個方法修改值對象引用去指向新的對象。你應該總是創建新的對象而不是修改原來的值對象,即使它是可變的。

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}
自動驗證

通常,KVC協議方法和KVC的默認實現都沒有執行自動驗證。你要適當地在APP應用這些驗證方法。

其他的cocoa技術可能會執行自動驗證。例如Core Data會在上下文保存時自動驗證屬性。


訪問搜索模式

默認實現KVC協議的NSObject對象會使用一定的規則映射鍵來訪問屬性。這些協議方法使用鍵參數來搜索實例的訪問方法和實例變量以及相關符合一些命名規則的方法。雖然你可以修改默認的搜索,瞭解默認搜索是怎樣執行的會對你有幫助。例如跟蹤KVC對象的行爲和更好地定義符合KVC的類。

注意:下面的說明會使用<key>作爲鍵的佔位符。

基本獲取方法的搜索模式

默認valueForKey:的實現使用key來獲取屬性名爲key的值。

1. 按順序尋找對象的get<Key>,<key>,is<Key>,或者_<key>訪問方法,如果發現,進行步驟5,否則進行下一步。

2. 如果沒有發現簡單訪問方法,尋找countOf<Key>和objectIn<Key>AtIndex:(對應NSArray的原始方法)或者<key>AtIndexes:方法 (對應NSArray的objectsAtIndexes:)。

如果發現第一個方法和後面兩個方法的其中一個,那麼會創建一個集合代理對象,這個對象會響應所有NSArray聲明的方法。否則進行步驟3。

這個代理對象會進行一系列NSArray方法的轉換,這些方法對應調用countOf<Key>和objectIn<Key>AtIndex:或者<key>AtIndexes:的其中一個方法的組合。如果原始對象實現可選方法get<Key>:range:,代理對象也會在適當時使用這個方法。實際上,代理對象會和KVC對象互相作用,就像KVC對象擁有一個NSArray屬性(對應代理對象),雖然對象沒有這個屬性。

3. 如果沒有發現簡單訪問方法和一組數組訪問方法,尋找countOf<Key>,enumeratorOf<Key>,memberOf<Key>: 方法(對應NSSet的原始方法)。

如果這3個方法都發現,那麼會創建一個集合代理對象,這個對象會響應所有NSSet聲明的方法。否則進行步驟4。

這個代理對象會進行一系列NSSet方法的轉換,這些方法對應調用countOf<Key>,enumeratorOf<Key>,memberOf<Key>: 的組合。實際上,代理對象會和KVC對象互相作用,就像KVC對象擁有一個NSSet屬性(對應代理對象),雖然對象沒有這個屬性。

4. 如果沒有發現簡單訪問方法和一組集合訪問方法而且KVC對象的類屬性accessInstanceVariablesDirectly爲YES,按順序尋找對象的_<key>,_is<Key>,<key>,或者is<Key>實例變量。如果發現,直接獲取實例變量的值並進行步驟5,否則進行步驟6。

5. 如果獲取的屬性值是對象指針,簡單地返回它。

如果屬性值是支持NSNumber的數值,創建NSNumber對象並返回它。

如果屬性值不支持NSNumber,轉換爲NSValue對象並返回它。

6. 如果都失敗了,調用valueForUndefinedKey:方法。這個方法默認會拋出異常,你可以在子類重寫這個方法來處理這種情況。

基本設置方法的搜索模式

默認setValue:forKey:的實現使用key和value來設置屬性名爲key的值。

1. 按順序尋找對象的set<Key>: 或者_set<Key>訪問方法。如果發現,調用這個方法來設置值(值可能需要解包裝)。

2. 如果沒有發現簡單訪問方法而且KVC對象的類屬性accessInstanceVariablesDirectly爲YES,按順序搜索對象的_<key>,_is<Key>,<key>,或者is<Key>實例變量。如果發現,直接設置實例變量的值。

3. 如果沒有發現簡單訪問方法和實例變量,調用setValue:forUndefinedKey:方法。這個方法默認會拋出異常,你可以在子類重寫這個方法來處理這種情況。

可變數組的搜索模式

默認mutableArrayValueForKey:的實現使用key獲取屬性名爲key的可變代理數組。

1. 尋找insertObject:in<Key>AtIndex:和removeObjectFrom<Key>AtIndex:方法(對應NSMutableArray的insertObject:atIndex:和removeObjectAtIndex:原始方法)或者insert<Key>:atIndexes:和remove<Key>AtIndexes:方法(對應NSMutableArray的insertObjects:atIndexes:和removeObjectsAtIndexes:原始方法)。

如果對象至少有一個插入方法和至少一個刪除方法,返回一個集合代理對象,這個對象響應NSMutableArray的所有方法。

這個代理對象會進行一系列NSMutableArray方法的轉換,這些方法對應調用insertObject:in<Key>AtIndex:,removeObjectFrom<Key>AtIndex:,insert<Key>:atIndexes:,remove<Key>AtIndexes:的結合。

當KVC對象實現可選方法replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法,代理對象會在適當的時候使用這些方法來獲取最好的性能。

2. 如果沒有發現可變數組方法,尋找set<Key>:訪問方法。在這種情況下,返回代理對象響應NSMutableArray方法,代理對象通過調用set<Key>:方法來響應這些方法。

注意:在第2步中返回的代理對象比第1步返回的代理對象效率要低,因爲會重複地創建新的集合對象而不是刪除和修改已經創建的集合對象。因此,你應該儘量避免這樣做。

3. 如果沒有發現可變數組方法和set<Key>:訪問方法而且KVC對象的類屬性accessInstanceVariablesDirectly爲YES,按順序尋找實例變量_<key>或者<key>。

如果發現這些實例變量,返回代理對象響應NSMutableArray方法,代理對象會轉發消息給實例變量,這個實例變量可能是NSMutableArray實例或者子類。

4. 如果都失敗,返回代理對象。如果向代理髮送NSMutableArray方法,會調用對象的setValue:forUndefinedKey:方法。這個方法默認會拋出異常,你可以在子類重寫這個方法來處理這種情況。

可變有序集合的搜索模式

默認mutableOrderedSetValueForKey: 的實現使用key獲取屬性名爲key的可變有序集合。

1. 尋找insertObject:in<Key>AtIndex:和removeObjectFrom<Key>AtIndex:方法(對應NSMutableOrderedSet的原始方法)以及insert<Key>:atIndexes: 和remove<Key>AtIndexes:方法(對應NSMutableOrderedSet的insertObjects:atIndexes:和removeObjectsAtIndexes:方法)。

如果對象至少有一個插入方法和至少一個刪除方法,返回一個集合代理對象,這個對象響應NSMutableOrderedSet的所有方法。

這個代理對象會進行一系列NSMutableOrderedSet方法的轉換,這些方法對應調用insertObject:in<Key>AtIndex:,removeObjectFrom<Key>AtIndex:,insert<Key>:atIndexes:,remove<Key>AtIndexes:的結合。

當KVC對象實現可選方法replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法,代理對象會在適當的時候使用這些方法來獲取最好的性能。

2. 如果沒有發現可變數組方法,尋找set<Key>:訪問方法。在這種情況下,返回代理對象響應NSMutableOrderedSet方法,代理對象通過調用set<Key>:方法來響應這些方法。

注意:在第2步中返回的代理對象比第1步返回的代理對象效率要低,因爲會重複地創建新的集合對象而不是刪除和修改已經創建的集合對象。因此,你應該儘量避免這樣做。

3. 如果沒有發現可變數組方法和set<Key>:訪問方法而且KVC對象的類屬性accessInstanceVariablesDirectly爲YES,按順序尋找實例變量_<key>或者<key>。

如果發現這些實例變量,返回代理對象響應NSMutableOrderedSet方法,代理對象會轉發消息給實例變量,這個實例變量可能是NSMutableOrderedSet實例或者子類。

4. 如果都失敗,返回代理對象。如果向代理髮送NSMutableOrderedSet方法,會調用對象的setValue:forUndefinedKey:方法。這個方法默認會拋出異常,你可以在子類重寫這個方法來處理這種情況。
可變集合的搜索模式

默認mutableSetValueForKey:的實現使用key獲取屬性名爲key的可變集合。

1. 尋找add<Key>Object:和remove<Key>Object:方法(對應NSMutableSet的addObject:和removeObject:原始方法)以及add<Key>:和remove<Key>:方法(對應NSMutableSet的unionSet:和minusSet:方法)。

如果對象至少有一個添加方法和至少一個刪除方法,返回一個集合代理對象,這個對象響應NSMutableSet的所有方法。

這個代理對象會進行一系列NSMutableSet方法的轉換,這些方法對應調用add<Key>Object:,remove<Key>Object:,add<Key>:,remove<Key>:的結合。

當KVC對象實現可選方法intersect<Key>: 或者set<Key>:方法,代理對象會在適當的時候使用這些方法來獲取最好的性能。

2. 如果KVC對象是Core Data管理對象,這個搜索模式不會繼續下去。

3. 如果沒有發現可變集合方法而且也不是Core Data管理對象,尋找set<Key>:訪問方法。在這種情況下,返回代理對象響應NSMutableSet方法,代理對象通過調用set<Key>:方法來響應這些方法。

注意:在第3步中返回的代理對象比第1步返回的代理對象效率要低,因爲會重複地創建新的集合對象而不是刪除和修改已經創建的集合對象。因此,你應該儘量避免這樣做。

4. 如果沒有發現可變集合方法和set<Key>:訪問方法而且KVC對象的類屬性accessInstanceVariablesDirectly爲YES,按順序尋找實例變量_<key>或者<key>。

如果發現這些實例變量,返回代理對象響應NSMutableArray方法,代理對象會轉發消息給實例變量,這個實例變量可能是NSMutableSet實例或者子類。

5. 如果都失敗,返回代理對象。如果向代理髮送NSMutableSet方法,會調用對象的setValue:forUndefinedKey:方法。這個方法默認會拋出異常,你可以在子類重寫這個方法來處理這種情況。


符合基本的KVC

當對象實現KVC,你依賴於NSObject的默認實現。默認實現也依賴於你定義的實例變量和訪問方法。

你可以使用@property來定義屬性,編譯器會自動生成實例變量和訪問方法而且會符合KVC默認實現的要求。

如果你手動定義實例變量和訪問方法,你需要符合KVC默認實現的約定。你可以添加額外方法來提高對象的集合屬性的交互和支持屬性驗證。

基本獲取方法

你可以在獲取方法添加額外的處理,你可以使用屬性名作爲方法名。

- (NSString*)title
{
   // Extra getter logic…
 
   return _title;
}

對於布爾值,你需要添加is前綴。

- (BOOL)isHidden
{
   // Extra getter logic…
 
   return _hidden;
}

如果屬性值是數值或者結構體,你不需要添加特殊處理,因爲KVC默認實現會包裝和解包裝它。

基本設置方法

你可以在設置方法添加額外的處理,你需要在屬性名前添加set前綴作爲設置方法名。

- (void)setHidden:(BOOL)hidden
{
    // Extra setter logic…
 
   _hidden = hidden;
}

注意:不要在設置方法調用屬性驗證方法。

當屬性值不是對象屬性,如果在KVC設置方法使用nil設置,會觸發setNilValueForKey:方法。

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"hidden"]) {
        [self setValue:@(NO) forKey:@”hidden”];
    } else {
        [super setNilValueForKey:key];
    }
}

你可以適當地提供上面的方法,即使你讓編譯器同步生成設置方法。

實例變量

當KVC默認實現沒有發現任何訪問方法,它會檢查accessInstanceVariablesDirectly的類實例變量是否爲YES來直接訪問實例變量。默認這個值爲YES,你可以在子類重寫爲NO。

如果你允許訪問實例變量,在屬性名加下滑線前綴作爲實例變量名。通常編譯器會自動生成這種形式的實例變量,你可以使用@synthesize指令來使用自己定義的實例變量名。

@synthesize title = _title;
在一些情況下,不是使用@synthesize指令和編譯器自動生成屬性,而是使用@dynamic指令來告訴編譯器自己會在運行時提供獲取和設置訪問方法。這樣就可以避免自動同步獲取方法以便你可以提供集合獲取方法。在這種情況下,你還要在接口聲明中聲明實例變量。
@interface MyObject : NSObject {
    NSString* _title;
}
 
@property (nonatomic) NSString* title;
 
@end
定義集合訪問方法

當你使用約定的命名來聲明實例變量和訪問方法,KVC默認實現會定位到它們並響應KVC聲明的方法。這同樣適用於一對多關係的集合屬性。但是,你可以提供集合訪問方法來實現一對多關係集合屬性。

• 不使用NSArray或者NSSet的一對多關係。當你提供集合訪問方法,KVC默認實現會返回一個代理對象,這個代理對象會調用這些方法來響應NSArray或者NSSet方法。內部操作的屬性對象可以不是NSArray或者NSSet,因爲代理對象會使用你的集合訪問方法來提供預期的行爲。

• 提高使用一對多關係的可變版本的性能。在響應每次變化時,不是使用設置方法來重複創建新的集合對象,而是使用你的集合訪問方法來修改你內部的屬性。

• 符合KVO來允許訪問你的集合屬性內容。

根據你需要實現索引、有序的集合(NSArray)還是無序、唯一的集合(NSSet),你可以實現這2組集合訪問方法的其中一組。在其他情況,你至少有一套獲取訪問方法來讀取屬性值和添加額外一組方法激活可變集合內容。

訪問有序集合

你添加有序訪問方法來支持計數,獲取,添加,替換有序集合對象。內部對象一般是NSArray或者NSMutableArray,但你可以提供集合訪問方法來激活任何屬性,這個屬性會看起來像數組。

有序集合的獲取方法

集合屬性默認沒有獲取方法,如果你提供有序集合的獲取方法,KVC默認實現會在valueForKey:返回數組代理對象,它會調用集合訪問方法來完成工作。

注意:編譯器默認會同步每一個屬性,所以默認實現不會返回代理對象。你可以不聲明屬性(完全依賴實例變量)或者使用@dynamic聲明屬性,來告訴編譯器在運行時提供訪問方法。這樣編譯器不會提供獲取方法而且會使用下面的集合訪問方法。

• countOf<Key>

這個方法返回一對多關係對象的數量,就像NSArray的原始方法count。當你的內部屬性爲NSArray,你可以直接使用這個方法。

- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}

• objectIn<Key>AtIndex:或者<key>AtIndexes

- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}
 
- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}

• get<Key>:range:

- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
               range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}
可變有序集合

支持一對多可變關係的有序訪問需要實現一組不同的訪問方法。當你提供這些設置方法,默認實現會響應mutableArrayValueForKey:方法並返回NSMutableArray代理對象,這個代理對象會使用你的集合訪問方法。這比直接返回NSMutableArray對象更有效。這個一對多可變關係也符合KVO。

爲了實現可變有序的一對多關係,實現下面額外方法:

• insertObject:in<Key>AtIndex:或者insert<Key>:atIndexes:

- (void)insertObject:(Transaction *)transaction
  inTransactionsAtIndex:(NSUInteger)index {
    [self.transactions insertObject:transaction atIndex:index];
}
 
- (void)insertTransactions:(NSArray *)transactionArray
              atIndexes:(NSIndexSet *)indexes {
    [self.transactions insertObjects:transactionArray atIndexes:indexes];
}

• removeObjectFrom<Key>AtIndex:或者remove<Key>AtIndexes:

- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index {
    [self.transactions removeObjectAtIndex:index];
}
 
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self.transactions removeObjectsAtIndexes:indexes];
}

• replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:

可選地提供這些方法可以提高性能。

- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index
                             withObject:(id)anObject {
    [self.transactions replaceObjectAtIndex:index
                              withObject:anObject];
}
 
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
                    withTransactions:(NSArray *)transactionArray {
    [self.transactions replaceObjectsAtIndexes:indexes
                                withObjects:transactionArray];
}
獲取無序集合

你添加無序集合訪問方法來提供訪問可變無序集合。通常,這個關係是NSSet或者NSMutableSet實例。但是,當你實現這些訪問方法,你可以使用任何類(包括NSSet、NSMutableSet)來實現這種關係,KVC默認實現返回的代理對象的行爲像NSSet一樣。

無序集合獲取方法

當你提供下面的集合獲取方法去返回集合的對象數量,遍歷集合對象,檢查集合是否存在對象。KVC默認實現會響應valueForKey:方法並返回NSSet代理對象,這個代理對象使用這些獲取方法來進行工作。

注意:編譯器默認會同步每一個屬性,所以默認實現不會返回代理對象。你可以不聲明屬性(完全依賴實例變量)或者使用@dynamic聲明屬性,來告訴編譯器在運行時提供訪問方法。這樣編譯器不會提供獲取方法而且會使用下面的集合訪問方法。

• countOf<Key>

- (NSUInteger)countOfEmployees {
    return [self.employees count];
}

• enumeratorOf<Key>

- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}

• memberOf<Key>:

- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}
可變無序集合

支持一對多可變關係的無序訪問需要實現額外的訪問方法。實現這些可變無序訪問方法允許你響應mutableSetValueForKey:方法並返回可變無序集合代理對象。實現這些訪問方法比依賴一個直接返回可變對象來改變數據更高效。這個一對多可變關係也符合KVO。

爲了實現可變有序的一對多關係,實現下面額外方法:

• add<Key>Object:或者add<Key>:

- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}
 
- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}

• remove<Key>Object:或者remove<Key>:

- (void)removeEmployeesObject:(Employee *)anObject {
    [self.employees removeObject:anObject];
}
 
- (void)removeEmployees:(NSSet *)manyObjects {
    [self.employees minusSet:manyObjects];
}
• intersect<Key>:

可選地提供這個方法可以提高性能。

- (void)intersectEmployees:(NSSet *)otherObjects {
    return [self.employees intersectSet:otherObjects];
}


處理非對象值

通常,KVC默認實現會包裝和解包裝非對象值。但是你可以處理非對象屬性設置nil值的情況。

這種情況下,KVC默認實現會調用setNilValueForKey:方法並拋出NSInvalidArgumentException異常,你可以在子類重寫這個方法來處理這種情況。

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}


添加驗證

KVC協議定義使用鍵或者鍵路徑來驗證屬性的方法。默認實現依賴於你實現的validate<Key>:error:方法;這個key對應屬性名。

如果你沒有實現屬性的驗證方法,默認實現會認爲驗證成功。這意味着你可以可選的爲某個屬性添加驗證。

實現驗證方法

當你提供屬性的驗證方法,這個方法接收2個參數的引用:需要驗證的值對象和用於返回錯誤信息的錯誤對象。你的驗證方法可能會執行3種行爲:

• 當值對象是有效的,返回YES而且不修改值對象或者錯誤對象。

• 當值對象是無效的而且你不想提供另一個有效的值,返回NO並提供一個NSError對象來指示失敗原因。

• 當值對象是無效的,但你可以提供一個有效的值,創建有效對象並賦值給值對象引用,返回YES並不改變錯誤對象。如果你提供另一個值對象,你應該總是返回新的值對象而不是修改原來需要驗證的值對象,即使這個值對象是可變的。

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}
驗證數值
- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    if (*ioValue == nil) {
        // Value is nil: Might also handle in setNilValueForKey
        *ioValue = @(0);
    } else if ([*ioValue floatValue] < 0.0) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidAgeCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Age cannot be negative" }];
        }
        return NO;
    }
    return YES;
}


描述屬性關係

類描述提供一個方法來描述一對一和一對多關係屬性。定義這些屬性的關係允許KVC更聰明和靈活地處理。

類描述

NSClassDescription是一個提供獲取類的元數據接口的基類。類描述對象記錄可用屬性(attribute)和這個類的對象與其他對象的關係(一對一,一對多,或者相反)。例如attributeKeys方法返回類定義的一組屬性;toManyRelationshipKeys方法和toOneRelationshipKeys方法返回一組一對一關係的鍵和一對多關係的鍵;inverseRelationshipKey方法返回提供鍵的相反方向的關係。

NSClassDescription沒有定義定義關係的方法。具體的子類必須定義這些方法。一旦創建,你必須使用NSClassDescription的registerClassDescription:forClass:類方法來註冊類描述的類。

NSScriptClassDescription是cocoa中唯一提供的具體子類,這封裝了應用的腳本信息。


性能設計

KVC是高效的,特別是當你使用默認實現來完成大量的工作,但是這會比直接調用方法稍微慢些。只有在使用KVC可以提高靈活性的情況下使用或者在其他cocoa技術依賴KVC時使用。

重寫KVC方法

通常,你只需要繼承NSObject來獲取KVC特性並定義符合KVC命名的屬性和訪問方法。你可能需要重寫KVC默認實現的訪問方法,例如valueForKey:和setValue:forKey:或者基於鍵的驗證方法validateValue:forKey:。因爲這些實現會緩存運行時環境的信息來提高效率。如果你重寫它們來添加額外的邏輯,確保你在運行前調用父類的默認實現。

優化一對多關係

當你實現一對多關係,在很多情況下,有序形式的訪問方法提供重要的性能,特別對於可變集合。


符合KVC約定的檢查表

下面的總結確保你符合KVC約定。

屬性(attribute)和一對一關係的符合

對於每一個屬性或者一對一關係的屬性:

√實現<key>或者is<Key>方法,或者創建<key>或者is<Key>實例變量。當自動同步屬性時,編譯器通常會幫你完成。

注意:通常屬性名稱的命名爲小寫開頭,默認實現同樣可以處理大寫開頭的屬性,例如URL。

√如果屬性是可變的,實現set<Key>:方法。當自動同步屬性時,編譯器通常會幫你完成。

注意:當你重寫默認的設置方法,確保不要調用任何的屬性驗證方法。

√如果屬性值是數值,重寫setNilValueForKey:方法來處理設置nil值給非對象屬性。

有序一對多關係的符合

對於每一個有序一對多關係(例如NSArray):

√實現<key>方法來返回數組或者有一個<key>或者_<key>的數組實例變量。當自動同步屬性時,編譯器通常會幫你完成。

√另外的,實現countOf<Key>和objectIn<Key>AtIndex:和<key>AtIndexes:的其中一個。

√可選的,實現get<Key>:range:方法來提高性能。

如果屬性是可變的,還要實現:

√實現一個或者同時兩個insertObject:in<Key>AtIndex:和insert<Key>:atIndexes:方法。

√實現一個或者同時兩個removeObjectFrom<Key>AtIndex:和remove<Key>AtIndexes:方法。

√可選的,實現replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法來提高性能。

無序一對多關係的符合

對於每一個無序一對多關係(例如NSSet):

√實現<key>方法來返回NSSet實例或者有一個<key>或者_<key>的NSSet實例變量。當自動同步屬性時,編譯器通常會幫你完成。

√另外的,實現countOf<Key>,enumeratorOf<Key>,memberOf<Key>:方法。

如果屬性是可變的,還要實現:

√實現一個或者同時兩個add<Key>Object:和add<Key>:。

√實現一個或者同時兩個remove<Key>Object:和remove<Key>:。

√可選的,實現intersect<Key>:方法來提高性能。

驗證

可選地需要驗證屬性:

√實現validate<Key>:error:方法,返回布爾值來指示值對象是否有效和錯誤對象的引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章