KVC

關(guān)于鍵值編碼

鍵值編碼(KVC)是一種由NSKeyValueCoding非正式協(xié)議提供的機(jī)制,對象采用該機(jī)制來提供對其屬性的間接訪問。當(dāng)對象兼容鍵值編碼時(shí),可以使用簡潔、統(tǒng)一的接口和字符串參數(shù)來訪問其屬性。這種間接訪問機(jī)制補(bǔ)充了實(shí)例變量和其關(guān)聯(lián)的訪問器方法提供的直接訪問。

通常情況下,使用訪問器方法來訪問對象的屬性。get訪問器(或者getter)返回屬性的值,set訪問器(或者setter)設(shè)置屬性的值。在Objective-C中,還可以直接訪問屬性的實(shí)例變量。以任何方式訪問對象屬性都很簡單,但需要調(diào)用屬性特定的方法或者變量名稱。隨著屬性列表的增長或改變,訪問這些屬性的代碼也必須如此。 相反,兼容鍵值編碼的對象提供了一個(gè)簡單的消息傳遞接口,該接口在其所有屬性中都是一致的。

鍵值編碼是一個(gè)基本概念,是許多其他Cocoa技術(shù)的基礎(chǔ),例如KVO、Cocoa綁定、Core Data和AppleScript-ability。在某些情況下,鍵值編碼還有助于簡化代碼。

訪問對象屬性

對象通常在其接口聲明中指定屬性,并且這些屬性屬于以下幾種類別之一:

  • Attributes:這些是簡單值,例如標(biāo)量、字符串和或者布爾值。諸如NSNumber之類的值對象和諸如NSColor之類的其他不可變類型也被視為屬性。
  • To-one relationships:這些是具有自己屬性的可變對象,對象的屬性可以在對象本身不變的情況下更改。例如,一個(gè)BankAccount對象可能具有一個(gè)owner屬性,該屬性是People對象的實(shí)例,owner屬性本身具有一個(gè)address屬性。在BankAccount對象對owner屬性的引用不變的情況下,owneraddress屬性可能會更改。換句話說,銀行賬戶的所有者沒有變更,但所有者的地址可能變了。
  • To-many relationships:這些是集合對象。通常是NSArray或者NSSet的實(shí)例,也可以是自定義集合類。

以下代碼中聲明的BankAccount對象演示了每種類型的屬性之一。

@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

為了保持封裝性,對象通常為其接口上的屬性提供訪問器方法??梢燥@式地編寫這些方法,也可以依賴編譯器自動(dòng)合成它們。無論哪種方式,必須在編譯之前將屬性名稱寫入代碼中。例如,前面聲明的BankAccount對象,編譯器會合成一個(gè)可以給myAccount實(shí)例調(diào)用的setter:

[myAccount setCurrentBalance:@(100.0)];

這樣雖然直接,但是缺乏靈活性。另一方面,兼容鍵值編碼的對象提供了使用字符串標(biāo)識符訪問對象屬性的更通用機(jī)制。

使用鍵(Key)和鍵路徑(Key Path)標(biāo)識對象的屬性

鍵是標(biāo)識特定屬性的字符串。按照慣例,表示屬性的鍵是代碼中顯示的屬性本身的名稱。鍵必須使用ASCII編碼,不能包含空格,并且通常以小寫字母開頭。

由于BankAccount類是兼容鍵值編碼的,所以它可以識別鍵owner、currentBalancetransactions,它們是其屬性的名稱??梢酝ㄟ^鍵代替調(diào)用setCurrentBalance:方法為currentBalance屬性設(shè)置值:

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

實(shí)際上,可以使用鍵參數(shù)不同的相同方法設(shè)置myAccount對象的所有屬性。因?yàn)閰?shù)是字符串,所以它是可以在運(yùn)行時(shí)操作的變量。

鍵路徑是一個(gè)使用點(diǎn)分割多個(gè)鍵的字符串,用于指定要遍歷的對象屬性序列。序列中第一個(gè)鍵的屬性是相對于接收者的,并且后面的鍵都是相對于其前面一個(gè)鍵所表示的屬性。鍵路徑對于使用單個(gè)方法深入到對象層次結(jié)構(gòu)是非常有用的。

例如,假設(shè)PersonAddress類也兼容鍵值編碼,那么應(yīng)用于myAccount實(shí)例的鍵路徑owner.address.street指的是存儲在銀行帳戶所有者地址中的街道字符串的值。

使用鍵獲取屬性值

當(dāng)對象遵循NSKeyValueCoding協(xié)議時(shí),其是兼容鍵值編碼的。繼承自NSObject(其提供了NSKeyValueCoding協(xié)議的必要方法的默認(rèn)實(shí)現(xiàn))的對象會自動(dòng)采用此協(xié)議的某些默認(rèn)行為。這樣的對象至少實(shí)現(xiàn)了以下基礎(chǔ)的基于鍵的getter:

  • valueForKey::返回接收者的與指定鍵對應(yīng)的屬性的值。如果根據(jù)訪問器查找方式中描述的規(guī)則無法找到key所指定的屬性,則該對象會向自身發(fā)送valueForUndefinedKey:消息。valueForUndefinedKey:方法的默認(rèn)實(shí)現(xiàn)會拋出一個(gè)NSUndefinedKeyException,但是子類可以覆蓋此行為并更優(yōu)雅地處理該情況。
  • valueForKeyPath::返回相對于接收者的指定鍵路徑對應(yīng)的屬性的值。鍵路徑序列中的任一對象不能兼容特定鍵的鍵值編碼——即其valueForKey:方法的默認(rèn)實(shí)現(xiàn)無法找到訪問器方法——該對象會接收到一個(gè)valueForUndefinedKey:消息。
  • dictionaryWithValuesForKeys::返回接收者的與鍵數(shù)組中每個(gè)鍵對應(yīng)的屬性的值。該方法為數(shù)組中的每個(gè)鍵調(diào)用valueForKey:方法,返回的NSDictionary包含數(shù)組中所有鍵的值。

注意:集合對象(如NSArray,NSDictionaryNSSet)不能包含nil作為值。相反,可以使用NSNull對象來表示nil值,NSNull提供了一個(gè)實(shí)例來表示對象屬性的nil值。dictionaryWithValuesForKeys:方法和相關(guān)的setValuesForKeysWithDictionary:方法的默認(rèn)實(shí)現(xiàn)自動(dòng)在NSNull(在字典參數(shù)中)和nil(在存儲屬性中)之間進(jìn)行轉(zhuǎn)換。

當(dāng)使用鍵路徑來尋址屬性時(shí),如果鍵路徑中的最后一個(gè)鍵的前一個(gè)鍵是to-many relationship(即它引用一個(gè)集合),則返回的值是一個(gè)包含集合中的每個(gè)對象的最后一個(gè)鍵所標(biāo)識屬性的值的集合。例如,請求鍵路徑transactions.payee會返回一個(gè)包含所有Transaction對象的payee實(shí)例的數(shù)組。這也適用于鍵路徑中的多個(gè)數(shù)組。例如,鍵路徑accounts.transactions.payee返回一個(gè)包含所有賬戶中所有交易的所有收款人對象的數(shù)組。

使用鍵設(shè)置屬性值

與getter一樣,兼容鍵值編碼的對象也提供了一組具有默認(rèn)行為的通用setter:

  • setValue:forKey::將消息接收者的與指定鍵對應(yīng)的屬性設(shè)置為給定值。setValue:forKey:方法的默認(rèn)實(shí)現(xiàn)自動(dòng)解包表示標(biāo)量和結(jié)構(gòu)體的NSNumberNSValue對象,并將它們分配給屬性。有關(guān)包裝和解包語義的詳細(xì)信息,請參看表示非對象值。如果消息接收對象沒有指定的鍵對應(yīng)的屬性,則該對象會向自身發(fā)送setValue:forUndefinedKey:消息。setValue:forUndefinedKey:方法的默認(rèn)實(shí)現(xiàn)拋出一個(gè)NSUndefinedKeyException。但是,子類可以重寫此方法以自定義方式處理請求。
  • setValue:forKeyPath::將相對于接收者的指定鍵路徑對應(yīng)的屬性設(shè)置為給定值。鍵路徑序列中的任何一個(gè)不兼容鍵值編碼的對象會收到setValue:forUndefinedKey:消息。
  • setValuesForKeysWithDictionary::使用字典鍵標(biāo)識屬性,使用字典中的設(shè)置屬性的值。其默認(rèn)實(shí)現(xiàn)會為每個(gè)鍵值對調(diào)用setValue:forKey:方法,并根據(jù)需要使用nil替換NSNull對象。

在默認(rèn)實(shí)現(xiàn)中,當(dāng)試圖將非對象屬性設(shè)置為nil時(shí),兼容鍵值編碼的對象會向自身發(fā)送setNilValueForKey:方法。setNilValueForKey:方法的默認(rèn)實(shí)現(xiàn)拋出一個(gè)NSInvalidArgumentException,但是對象可以覆蓋此行為以替換默認(rèn)值或標(biāo)記值,如處理非對象值中所述。

使用鍵簡化對象訪問

要了解基于鍵的getter和setter如何簡化代碼,請考慮以下示例。 在macOS中,NSTableViewNSOutlineView對象將標(biāo)識符字符串與其每個(gè)列相關(guān)聯(lián)。 如果支持表的模型對象不兼容鍵值編碼,則表的數(shù)據(jù)源方法將被強(qiáng)制檢查每個(gè)列標(biāo)識符來查找要返回的正確屬性,如下面代碼所示。 此外,當(dāng)未來向模型添加另一個(gè)屬性(在本例中Person對象)時(shí),還必須重新訪問數(shù)據(jù)源方法,添加另一個(gè)條件來測試新屬性并返回相關(guān)值。

- (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;
}

以下代碼顯示了相同數(shù)據(jù)源方法的更簡潔的實(shí)現(xiàn),該實(shí)現(xiàn)利用了兼容鍵值編碼的Person對象。僅使用列標(biāo)識符作為valueForKey:方法的鍵參數(shù)來獲取對應(yīng)的屬性值。除了更短之外,它還更通用,因?yàn)橹灰袠?biāo)識符始終與模型對象的屬性名稱匹配,它在以后添加新列時(shí)將繼續(xù)保持不變。

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

訪問集合屬性

兼容鍵值編碼的對象以與公開其他類型屬性相同的方式公開其To-many relationships類型的屬性??梢允褂?code>valueForKey:和setValue:forKey:方法來獲取和設(shè)置集合對象,就像任何其他對象一樣。但是,當(dāng)想要操縱這些集合的內(nèi)容時(shí),使用協(xié)議定義的可變代理方法通常是最有效的。

該協(xié)議為訪問集合對象定義了三種不同的代理方法,每種方法都有一個(gè)鍵和鍵路徑參數(shù):

  • mutableArrayValueForKey:mutableArrayValueForKeyPath::它們返回一個(gè)代理對象,該代理對象的行為類似于NSMutableArray對象。
  • mutableSetValueForKey:mutableSetValueForKeyPath::它們返回一個(gè)代理對象,該代理對象的行為類似于NSMutableSet對象。
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath::它們返回一個(gè)代理對象,該代理對象的行為類似于NSMutableOrderedSet對象。

當(dāng)對代理對象執(zhí)行向其添加對象和從中刪除或者替換對象的操作時(shí),協(xié)議的默認(rèn)實(shí)現(xiàn)會相應(yīng)地修改集合對象。這比使用valueForKey:方法獲取不可變的集合對象,根據(jù)該集合對象創(chuàng)建一個(gè)包含已修改的內(nèi)容的集合對象,然后使用setValue:forKey:方法將其存儲回對象更加有效。在許多情況下,它也比直接使用可變集合屬性更有效。這些方法為集合對象中保存的對象提供了保持KVO兼容的額外好處(有關(guān)詳細(xì)信息,請參看KVO)。

使用集合運(yùn)算符

當(dāng)向兼容鍵值編碼的對象發(fā)送valueForKeyPath:消息時(shí),可以在鍵路徑中嵌入一個(gè)集合運(yùn)算符。集合運(yùn)算符是一個(gè)開頭是@符號的關(guān)鍵字,它告知getter在返回之前應(yīng)該以某種方式操作數(shù)據(jù)。NSObject提供的valueForKeyPath:方法的默認(rèn)實(shí)現(xiàn)實(shí)現(xiàn)了這種行為。

當(dāng)鍵路徑包含一個(gè)集合運(yùn)算符時(shí),在運(yùn)算符之前的鍵路徑部分(成為左鍵路徑)表示valueForKeyPath消息的接收者對象的集合。如果將消息直接發(fā)送給集合對象(例如NSArray實(shí)例),則略去左鍵路徑。

運(yùn)算符之后的鍵路徑部分(稱為右鍵路徑)指定運(yùn)算符要操作的集合的屬性。除@count之外的所有集合運(yùn)算符都需要右鍵路徑。下圖說明了運(yùn)算符鍵路徑格式。

圖2-1 運(yùn)算符鍵路徑格式.png

集合運(yùn)算符表現(xiàn)出三種基本類型的行為:

  • 聚合運(yùn)算符以某種方式合并集合中的對象,并返回與右鍵路徑中指定的屬性的數(shù)據(jù)類型相匹配的單個(gè)對象。@count運(yùn)算符是一個(gè)例外,其沒有右鍵路徑,并總是返回一個(gè)NSNumber實(shí)例。
  • 數(shù)組運(yùn)算符返回一個(gè)包含指定的集合中所保存對象的子集的NSArray實(shí)例。
  • 嵌套運(yùn)算符處理包含其他集合的集合,并返回一個(gè)NSArray或者NSSet實(shí)例,具體取決于運(yùn)算符,它以某種方式合并嵌套集合中的對象。

樣本數(shù)據(jù)

以下描述包括演示如何調(diào)用每個(gè)運(yùn)算符的代碼片段,以及執(zhí)行此運(yùn)算的結(jié)果。它們依賴于上文提到的BankAccount類,它包含一個(gè)含有Transaction對象的數(shù)組。每個(gè)Transaction對象代表一個(gè)簡單的支票簿條目,如下所示。

@interface Transaction : NSObject

@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When

@end

為了討論,假設(shè)現(xiàn)在有一個(gè)BankAccount實(shí)例有一個(gè)填充了下表所示數(shù)據(jù)的transactions數(shù)組。

payee values amount values formatted as currency date values formatted as month day, year
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

聚合運(yùn)算符

聚合運(yùn)算符可以處理數(shù)組或者屬性集,生成反映集合的某些方面的單個(gè)值。

@avg

當(dāng)指定@avg運(yùn)算符時(shí),valueForKeyPath:方法會讀取集合中每個(gè)元素的右鍵路徑指定的屬性,將其轉(zhuǎn)換為double(用0替換nil值),并計(jì)算這些值的算數(shù)平均值,然后將結(jié)果存儲在一個(gè)NSNumber實(shí)例中并返回該結(jié)果。

獲取樣本數(shù)據(jù)的平均交易金額:

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

transactionAverage的格式化結(jié)果為$456.54。

@count

當(dāng)指定@count運(yùn)算符時(shí),valueForKeyPath:方法使用一個(gè)NSNumber實(shí)例來返回集合中的對象數(shù)量。右鍵路徑(如果存在)將被忽略。

獲取transactions數(shù)組中Transaction對象的數(shù)量:

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

numberOfTransactions的值是13。

@max

當(dāng)指定@max運(yùn)算符時(shí),valueForKeyPath:方法查找右鍵路徑指定的集合元素的屬性,并返回值最大的一個(gè)。查找時(shí)使用compare:方法進(jìn)行比較,許多Foundation類定義了該方法,例如NSNumber類。因此,右鍵路徑標(biāo)識的屬性必須持有一個(gè)能夠?qū)?code>compare:消息進(jìn)行響應(yīng)的對象。查找會忽略值為nil的屬性。

獲取樣本數(shù)據(jù)中Transaction對象的date屬性的最大值:

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

latestDate的格式化值為Jul 15, 2016。

@min

當(dāng)指定@min運(yùn)算符時(shí),valueForKeyPath:方法查找右鍵路徑指定的集合元素的屬性,并返回值最小的一個(gè)。查找時(shí)使用compare:方法進(jìn)行比較,許多Foundation類定義了該方法,例如NSNumber類。因此,右鍵路徑標(biāo)識的屬性必須持有一個(gè)能夠?qū)?code>compare:消息進(jìn)行響應(yīng)的對象。查找會忽略值為nil的屬性。

獲取樣本數(shù)據(jù)中Transaction對象的date屬性值的最小值:

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

earliestDate的格式化值為Dec 1, 2015。

@sum

當(dāng)指定@sum運(yùn)算符時(shí),valueForKeyPath:方法讀取集合中每個(gè)元素的右鍵路徑指定的屬性,將其轉(zhuǎn)換為double(用0替換nil值),并計(jì)算這些值的總和,然后將結(jié)果存儲在一個(gè)NSNumber實(shí)例中并返回該結(jié)果。

獲取樣本數(shù)據(jù)的交易總金額:

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

amountSum的格式化結(jié)果為$5,935.00。

數(shù)組運(yùn)算符

數(shù)組運(yùn)算符會使得valueForKeyPath:方法返回一個(gè)與右鍵路徑標(biāo)識的特定對象集對應(yīng)的對象數(shù)組。

重要:如果使用數(shù)組運(yùn)算符時(shí),任何葉對象為nil,valueForKeyPath:方法會引發(fā)異常。

@distinctUnionOfObjects

當(dāng)指定@distinctUnionOfObjects運(yùn)算符時(shí),valueForKeyPath:方法創(chuàng)建并返回一個(gè)數(shù)組,該數(shù)組包含與右鍵路徑標(biāo)識的屬性對應(yīng)的集合的不同對象。

獲取樣本數(shù)據(jù)中transactions數(shù)組中所有Transaction對象的不同的payee屬性值的集合:

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

生成的distinctPayees數(shù)組包含Car Loan,General Cable,Animal Hospital,Green Power,Mortgage。

@unionOfObjects

當(dāng)指定@unionOfObjects運(yùn)算符時(shí),valueForKeyPath:方法創(chuàng)建并返回一個(gè)數(shù)組,該數(shù)組包含與右鍵路徑標(biāo)識的屬性對應(yīng)的集合中的所有對象。@distinctUnionOfObjects不同,其不會刪除重復(fù)的對象

獲取樣本數(shù)據(jù)中transactions數(shù)組中所有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。

嵌套運(yùn)算符

嵌套運(yùn)算符對嵌套集合進(jìn)行操作,該集合的每個(gè)條目都包含一個(gè)集合。

重要:如果使用嵌套運(yùn)算符時(shí),任何葉對象為nil,valueForKeyPath:方法會引發(fā)異常。

為方便描述,假設(shè)存在填充了以下數(shù)據(jù)的被稱為moreTransactions的第二個(gè)數(shù)據(jù)數(shù)組,并于上文中起始的transactions數(shù)組一起收集到一個(gè)嵌套數(shù)組中:

NSArray* moreTransactions = @[transactionData];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
payee values amount values formatted as currency date values formatted as month day, year
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 Jul 14, 2016

@distinctUnionOfArrays

當(dāng)指定@distinctUnionOfArrays運(yùn)算符時(shí),valueForKeyPath:方法創(chuàng)建并返回一個(gè)數(shù)組,該數(shù)組包含與右鍵路徑標(biāo)識的屬性對應(yīng)的所有集合的組合的不同(刪除重復(fù)項(xiàng))對象。

獲取arrayOfArrays數(shù)組中的所有數(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

當(dāng)指定@unionOfArrays運(yùn)算符時(shí),valueForKeyPath:方法創(chuàng)建并返回一個(gè)數(shù)組,該數(shù)組包含與右鍵路徑標(biāo)識的屬性對應(yīng)的所有集合的組合的所有(不會刪除重復(fù)項(xiàng))對象。

獲取arrayOfArrays數(shù)組中的所有數(shù)組中的payee屬性的所有值:

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

生成的collectedPayees數(shù)組包含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

當(dāng)指定@distinctUnionOfSets運(yùn)算符時(shí),valueForKeyPath:方法創(chuàng)建并返回一個(gè)NSSet對象,該對象包含與右鍵路徑標(biāo)識的屬性對應(yīng)的所有集合的組合的不同對象。

此運(yùn)算符的行為與@distinctUnionOfArrays類似,不同之處在于它需要一個(gè)NSSet實(shí)例,該實(shí)例包含的也是NSSet實(shí)例,而不是一個(gè)包含NSArray實(shí)例的NSArray實(shí)例。 此外,它返回的也是一個(gè)NSSet實(shí)例。 假設(shè)示例數(shù)據(jù)已存儲在集合而不是數(shù)組中,示例調(diào)用和結(jié)果與@distinctUnionOfArrays顯示的相同。

表示非對象值

NSObject提供的鍵值編碼協(xié)議方法的實(shí)現(xiàn)同時(shí)支持對象屬性和非對象屬性。默認(rèn)實(shí)現(xiàn)自動(dòng)在對象參數(shù)或者返回值與非對象屬性之間進(jìn)行轉(zhuǎn)換。這使得即使存儲的屬性是標(biāo)量或者結(jié)構(gòu)體,基于鍵的setter和getter的簽名也保持一致。

當(dāng)調(diào)用協(xié)議的其中一個(gè)getter(例如valueForKey:)時(shí),默認(rèn)實(shí)現(xiàn)將根據(jù)訪問器查找方式中描述的規(guī)則確定特定的為指定鍵提供值的訪問器方法或者實(shí)例變量。如果返回值不是對象,則getter使用該值初始化一個(gè)NSNumber對象(對于標(biāo)量)或者NSValue對象(對于結(jié)構(gòu)體)并返回該值。

類似地,默認(rèn)情況下,setter(例如setValue:forKey:)在給定特定鍵時(shí)確定一個(gè)屬性的訪問器或者實(shí)例變量所需要的數(shù)據(jù)類型。如果數(shù)據(jù)類型不是對象類型,則setter首先向傳入的值對象發(fā)送一個(gè)適當(dāng)?shù)?code><type>Value消息來提取基礎(chǔ)數(shù)據(jù),并存儲該數(shù)據(jù)。

注意:當(dāng)使用非對象屬性的nil值調(diào)用鍵值編碼協(xié)議setter的其中一個(gè)時(shí),setter會向setter消息的接收對象發(fā)送一個(gè)setNilValueForKey:消息。該方法的默認(rèn)實(shí)現(xiàn)會引發(fā)NSInvalidArgumentException異常,但子類可以覆蓋此行為,如處理非對象值中所述,例如設(shè)置標(biāo)記值或者提供有意義的默認(rèn)值。

包裝和解包標(biāo)量類型

下表列出了默認(rèn)的鍵值編碼實(shí)現(xiàn)使用NSNumber實(shí)例包裝的標(biāo)量類型。對于每種數(shù)據(jù)類型,該表顯示了用于將基礎(chǔ)屬性值初始化為一個(gè)NSNumber實(shí)例以作為getter返回值的創(chuàng)建方法,還顯示了用于在設(shè)置操作期間從setter輸入?yún)?shù)中提取值的訪問器方法。

Data type Creation method Accessor method
BOOL numberWithBool: boolValue (in iOS)
charValue (in macOS)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

注意:在macOS中,由于歷史原因,BOOL被定義為signed char類型,而KVC不會區(qū)分這點(diǎn)。因此,當(dāng)key所標(biāo)識的屬性為BOOL類型時(shí),不應(yīng)該傳遞諸如@"ture"@"YES"這樣的字符串作為value給setValue:forKey:方法。否則,由于BOOLchar類型,KVC將嘗試調(diào)用charValue方法,但NSString沒有實(shí)現(xiàn)此方法,這會導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。取而代之的是,傳遞一個(gè)NSNumber對象,例如@(1)或者@(YES),作為setValue:forKey:的value參數(shù)。此限制不適用于iOS,iOS中的BOOL被定義為bool類型,而KVC調(diào)用boolValue方法,該方法適用于NSNumber對象或格式正確的NSString對象。

包裝和解包結(jié)構(gòu)體

下表顯示了默認(rèn)訪問器用于包裝和解包常見的NSPointNSRange、NSRectNSSize結(jié)構(gòu)體的創(chuàng)建方法和訪問器方法。

Data type Creation method Accessor method
NSPoint valueWithPoint: pointValue
NSRange valueWithRange: rangeValue
NSRect valueWithRect: (macOS only). rectValue
NSSize valueWithSize: sizeValue

自動(dòng)包裝和解包并不只限于NSPoint、NSRange、NSRectNSSize,結(jié)構(gòu)體類型可以包裝在NSValue對象中,如下所示。

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface MyClass

@property (nonatomic) ThreeFloats threeFloats;

@end

使用MyClass類的實(shí)例時(shí),可以使用鍵值編碼獲取threeFloats的值:

NSValue *result = [myClass valueForKey:@"threeFloats"];

同樣,可以使用鍵值編碼設(shè)置threeFloats的值:

hreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

驗(yàn)證屬性

鍵值編碼協(xié)議定義了支持屬性驗(yàn)證的方法。就像使用基于鍵的訪問器來讀取和寫入兼容鍵值編碼的對象的屬性一樣,也可以通過鍵(或鍵路徑)來驗(yàn)證屬性。當(dāng)調(diào)用validateValue:forKey:error:方法(或者validateValue:forKeyPath:error:方法)時(shí),協(xié)議的默認(rèn)實(shí)現(xiàn)會在接收驗(yàn)證消息的對象(或者鍵路徑標(biāo)識的屬性所屬對象)中查找方法名稱與格式validate<Key>:error:相匹配的方法。如果對象沒有此類方法,則默認(rèn)驗(yàn)證成功并且返回YES。當(dāng)存在特定于屬性的驗(yàn)證方法時(shí),默認(rèn)實(shí)現(xiàn)將返回調(diào)用該方法的結(jié)果。

注意:僅在Objective-C中使用屬性驗(yàn)證。

由于特定于屬性的驗(yàn)證方法通過引用接收值和錯(cuò)誤參數(shù),所以驗(yàn)證有三種可能的結(jié)果:

  • 驗(yàn)證方法認(rèn)為值對象有效并返回YES,且不會更改值對象或提示錯(cuò)誤。
  • 驗(yàn)證方法認(rèn)為值對象無效,但選擇不更改它。 在這種情況下,該方法返回NO并將錯(cuò)誤引用(如果調(diào)用者提供)指向一個(gè)NSError對象,該對象指示失敗的原因。
  • 驗(yàn)證方法認(rèn)為值對象無效,但會創(chuàng)建一個(gè)新的有效對象作為替補(bǔ)。 在這種情況下,該方法返回YES,同時(shí)將錯(cuò)誤引用指向一個(gè)NSError對象。 在返回之前,該方法修改值引用以指向新的值對象。 當(dāng)執(zhí)行修改時(shí),該方法總是創(chuàng)建一個(gè)新對象,而不是修改舊對象,即使值對象是可變的。

以下代碼顯示了如何為名稱字符串調(diào)用驗(yàn)證的示例:

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

自動(dòng)驗(yàn)證

通常情況下,鍵值編碼協(xié)議及其默認(rèn)實(shí)現(xiàn)都不會定義任何自動(dòng)執(zhí)行驗(yàn)證的機(jī)制。但是可以在應(yīng)用程序需要時(shí),手動(dòng)使用驗(yàn)證方法。

某些Cocoa技術(shù)在某些情況下會自動(dòng)執(zhí)行驗(yàn)證。例如,Core Data會在保存管理對象上下文時(shí)自動(dòng)執(zhí)行驗(yàn)證(請參看Core Data Programming Guide)。此外,在macOS中,Cocoa綁定允許我們指定應(yīng)自動(dòng)執(zhí)行驗(yàn)證(有關(guān)詳細(xì)信息,請參看Cocoa Bindings Programming Topics)。

訪問器查找方式

NSObject提供的NSKeyValueCoding協(xié)議的默認(rèn)實(shí)現(xiàn)使用明確定義的規(guī)則集將基于鍵的訪問器調(diào)用映射到對象的屬性。這些協(xié)議方法使用鍵參數(shù)在對象中查找訪問器、實(shí)例變量和遵循某些約定的相關(guān)方法。雖然很少需要修改此默認(rèn)查找,但了解它的工作方式能夠幫助我們跟蹤鍵值編碼對象的行為和使我們自己的對象兼容鍵值編碼。

注意:本節(jié)中的描述使用<key><Key>作為在鍵值編碼協(xié)議方法中的鍵字符串參數(shù)的占位符。協(xié)議方法將占位符用作輔助方法調(diào)用或變量名查找的一部分。映射的屬性名稱取決于占位符。例如,對于get訪問器<key>is<Key>,名為hidden的屬性映射到hiddenisHidden。

Getter的查找方式

給定一個(gè)鍵參數(shù)作為輸入,valueForKey:方法的默認(rèn)實(shí)現(xiàn)會執(zhí)行以下過程:

  1. valueForKey:消息的接收對象中按順序依次查找名為get<Key>、<key>is<Key>或者_<key>的訪問器方法。如果存在某個(gè)方法,則調(diào)用該方法并跳到第5步。否則,執(zhí)行第2步。

  2. 查找名為countOf<Key>、objectIn<Key>AtIndex:(對應(yīng)于NSArray類定義的原始方法)和<key>AtIndexes:(對應(yīng)于NSArrayobjectsAtIndexes:方法)的方法。如果找到第一個(gè)方法和其他兩個(gè)方法中的至少一個(gè),則創(chuàng)建一個(gè)能夠響應(yīng)NSArray類所有方法的集合代理對象,并返回該集合代理對象,查找完成。否則,執(zhí)行第3步。

我們在操作該集合代理對象時(shí),集合代理對象會將其接收的任何NSArray消息轉(zhuǎn)換為countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某種組合并發(fā)送給valueForKey:消息的接收對象。如果接收對象還實(shí)現(xiàn)了名為get<Key>:range:的可選方法,則集合代理對象也會在適當(dāng)時(shí)使用該方法。實(shí)際上,集合代理對象與兼容鍵值編碼的對象一起工作,使得兼容鍵值編碼的對象的集合屬性的行為就像該屬性是NSArray一樣,即使它并不是。

  1. 查找名為countOf<Key>、enumeratorOf<Key>memberOf<Key>:(對應(yīng)于NSSet類定義的原始方法)的三種方法。如果三種方法全部存在,則創(chuàng)建一個(gè)能夠響應(yīng)NSSet類所有方法的集合代理對象,并返回該集合代理對象,查找完成。否則執(zhí)行第4步。

我們在操作該集合代理對象時(shí),集合代理對象會將其接收的任何NSSet消息轉(zhuǎn)換為countOf<Key>、enumeratorOf<Key>memberOf<Key>:消息的某種組合并發(fā)送給valueForKey:消息的接收對象。實(shí)際上,集合代理對象與兼容鍵值編碼的對象一起工作,使得兼容鍵值編碼的對象的集合屬性的行為就像該屬性是NSSet一樣,即使它并不是。

  1. 如果沒有找到訪問器方法和集合訪問方法組,并且valueForKey:消息的接收對象的類方法accessInstanceVariablesDirectly返回YES,則按順序依次查找名為_<key>_is<Key>、<key>或者is<Key>的實(shí)例變量。如果存在,則直接獲取實(shí)例變量的值并執(zhí)行第5步。否則,執(zhí)行第6步。

  2. 如果檢索到的屬性值是一個(gè)對象指針,則返回該結(jié)果,查找完成。
    如果屬性值是NSNumber支持的標(biāo)量類型,則將其存儲在NSNumber實(shí)例中并返回該值,查找完成。
    如果屬性值是NSNumber不支持的標(biāo)量類型,則將其轉(zhuǎn)換為NSValue對象并返回該值,查找完成。

  3. 如果以上所有查找都失敗了,則調(diào)用valueForUndefinedKey:方法,查找完成。 默認(rèn)情況下,這會引發(fā)異常,但NSObject的子類可能會提供特定于鍵的行為。

Setter的查找方式

給定鍵和值參數(shù)作為輸入,setValue:forKey:方法的默認(rèn)實(shí)現(xiàn)會執(zhí)行以下過程:

  1. setValue:forKey:消息的接收對象中按順序依次查找名為set<Key>:或者_set<Key>的訪問器方法。如果存在某個(gè)方法,則使用輸入的值調(diào)用該方法來設(shè)置屬性值,查找完成。否則,執(zhí)行第2步。

  2. 如果沒有找到訪問器方法,并且setValue:forKey:消息的接收對象的類方法accessInstanceVariablesDirectly方法返回YES,則按順序依次查找名為_<key>_is<Key>、<key>或者is<Key>的實(shí)例變量。如果存在,則直接使用輸入的值來設(shè)置實(shí)例變量,查找完成。否則,執(zhí)行第3步。

  3. 如果沒有找到訪問器方法和實(shí)例變量,則調(diào)用setValue:forUndefinedKey:方法,查找完成。默認(rèn)情況下,這會引發(fā)異常,但NSObject的子類可能會提供特定于鍵的行為。

Mutable Array的查找方式

給定鍵和值參數(shù)作為輸入,mutableArrayValueForKey:方法的默認(rèn)實(shí)現(xiàn)為名稱為<key>的集合屬性返回一個(gè)可變代理數(shù)組,其會執(zhí)行以下過程:

  1. mutableArrayValueForKey:消息的接收對象中查找一對名為insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(對應(yīng)于NSMutableArray類的原始方法)的方法,或者一對名為insert<Key>:atIndexes:remove<Key>AtIndexes:(對應(yīng)于NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:方法)的方法。如果至少存在一對插入和刪除方法,則返回一個(gè)能夠響應(yīng)NSMutableArray消息的集合代理對象,查找完成。

我們在操作該集合代理對象時(shí),集合代理對象會將接收到的NSMutableArray消息轉(zhuǎn)換為insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes:消息的某種組合發(fā)送給mutableArrayValueForKey:消息的接收對象。當(dāng)接收對象實(shí)現(xiàn)了一個(gè)可選的名為replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:的替換對象方法時(shí),集合代理對象會在適當(dāng)時(shí)間使用它們以獲得最佳性能。

  1. 如果不存在一對插入和刪除方法,會查找名為set<Key>:的訪問器方法。如果存在該方法,則返回一個(gè)能夠響應(yīng)NSMutableArray消息的集合代理對象,查找完成。

該集合代理對象與第1步中返回的集合代理對象有所不同,其是通過發(fā)送set<Key>:消息給mutableArrayValueForKey:消息的接收對象來響應(yīng)NSMutableArray消息的。

注意:第2步中描述的機(jī)制比第1步的效率要低得多,因?yàn)樗赡苌婕爸貜?fù)創(chuàng)建新的集合對象而不是修改現(xiàn)有的集合對象。因此,在設(shè)計(jì)我們自己的兼容鍵值編碼的對象時(shí),通常應(yīng)該避免使用該機(jī)制。

  1. 如果可變數(shù)組方法和訪問器方法都不存在,并且mutableArrayValueForKey:消息的接收對象的類方法accessInstanceVariablesDirectly返回YES,則按順序依次查找名為_<key>或者<key>的實(shí)例變量。如果存在實(shí)例變量,則返回一個(gè)集合代理對象,查找完成。

我們在操作該集合代理對象時(shí),集合代理對象會將其接收到的所有NSMutableArray消息轉(zhuǎn)發(fā)給實(shí)例變量,該實(shí)例變量通常是NSMutableArray或其子類之一。

  1. 如果以上所有步驟都失敗,則返回一個(gè)可變集合代理對象,查找完成。

該集合代理對象在收到NSMutableArray的消息時(shí),向mutableArrayValueForKey消息的接收對象發(fā)送一個(gè)setValue:forUndefinedKey:消息,查找完成。setValue:forUndefinedKey:的默認(rèn)實(shí)現(xiàn)會引發(fā)一個(gè)NSUndefinedKeyException,但子類可能會覆蓋此行為。

Mutable Ordered Set的查找方式

mutableOrderedSetValueForKey:方法的默認(rèn)實(shí)現(xiàn)會識別與valueForKey:方法相同的訪問器方法和mutable ordered set訪問器方法,并遵循相同的直接訪問實(shí)例變量策略。但是,其返回的是一個(gè)可變集合代理對象,而valueForKey:方法返回的是一個(gè)不可變集合代理對象。此外,它還執(zhí)行以下操作:

  1. 在接收對象中查找一對名為insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(對應(yīng)于NSMutableOrderedSet類定義的原始方法)的方法,或者一對名為insert<Key>:atIndexes:remove<Key>AtIndexes:(對應(yīng)于NSMutableOrderedSet類的insertObjects:atIndexes:removeObjectsAtIndexes:)的方法。如果至少存在一對插入和刪除方法,則返回一個(gè)能夠響應(yīng)NSMutableOrderedSet消息的可變集合代理對象,查找完成。

我們在操作該可變集合代理對象時(shí),可變集合代理對象會將接收到的NSMutableOrderedSet消息轉(zhuǎn)換為insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:、insert<Key>:atIndexes:remove<Key>AtIndexes:消息的某種組合發(fā)送給mutableOrderedSetValueForKey:消息的接收對象。當(dāng)接收對象實(shí)現(xiàn)了一個(gè)可選的replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:的方法時(shí),可變集合代理對象也會在適當(dāng)時(shí)間使用它們。

  1. 如果未找到mutable ordered set方法,則查找名為set<Key>:的訪問器。在這種情況下,會返回一個(gè)可變集合代理對象,查找完成。

該可變集合代理對象每次接收到NSMutableOrderedSet消息時(shí),會發(fā)送一個(gè)set<Key>:消息給mutableOrderedSetValueForKey:消息的接收對象。

注意:第2步中描述的機(jī)制比第1步的效率要低得多,因?yàn)樗赡苌婕爸貜?fù)創(chuàng)建新的集合對象而不是修改現(xiàn)有的集合對象。因此,在設(shè)計(jì)我們自己的兼容鍵值編碼的對象時(shí),通常應(yīng)該避免使用該機(jī)制。

  1. 如果mutable ordered set方法和訪問器方法都不存在,并且mutableOrderedSetValueForKey:消息的接收對象的類方法accessInstanceVariablesDirectly返回YES,則按順序依次查找名為_<key>或者<key>的實(shí)例變量。如果存在實(shí)例變量,則返回一個(gè)可變集合代理對象,查找完成。

該可變集合代理對象會將其接收到的所有NSMutableOrderedSet消息轉(zhuǎn)發(fā)給實(shí)例變量,該實(shí)例變量通常是NSMutableOrderedSet或其子類之一。

  1. 如果以上所有步驟都失敗,則返回一個(gè)可變集合代理對象,查找完成。

該可變集合代理對象在收到NSMutableOrderedSet消息時(shí),向mutableOrderedSetValueForKey:消息的接收對象發(fā)送一個(gè)setValue:forUndefinedKey:消息。setValue:forUndefinedKey:的默認(rèn)實(shí)現(xiàn)會引發(fā)一個(gè)NSUndefinedKeyException,但子類可能會覆蓋此行為。

Mutable Set的查找方式

給定一個(gè)鍵參數(shù)作為輸入,mutableSetValueForKey:方法的默認(rèn)實(shí)現(xiàn)為調(diào)用對象的名為<key>的集合屬性返回一個(gè)可變集合代理對象,其會執(zhí)行以下過程:

  1. 查找一對名為add<Key>Object:remove<Key>Object:的方法(對應(yīng)于NSMutableSet的原始方法addObject:removeObject:),或者一對名為add<Key>:remove<Key>:的方法(對應(yīng)于NSMutableSetunionSet:minusSet:方法)。如果至少存在一對插入和刪除方法,則返回一個(gè)能夠響應(yīng)NSMutableSet消息的代理對象,查找完成。

我們在操作代理對象時(shí),代理對象會將接收到的NSMutableSet消息轉(zhuǎn)換為add<Key>Object:、remove<Key>Object:、addObject:removeObject:消息的某種組合發(fā)送給mutableSetValueForKey:消息的接收對象。當(dāng)接收對象實(shí)現(xiàn)了一個(gè)名為intersect<Key>:或者set<Key>:的方法時(shí),代理對象會在適當(dāng)時(shí)間使用它們以獲得最佳性能。

  1. 如果mutableSetValueForKey:消息的接收者是一個(gè)managed object,則查找模式不會像non-managed object那樣繼續(xù)。 有關(guān)詳細(xì)信息,請參看Core Data Programming Guide

  2. 如果未找到mutable set方法,并且mutableSetValueForKey:消息的接收對象不是一個(gè)managed object,則會查找名為set<Key>:的訪問器方法。如果存在該方法,則返回一個(gè)代理對象,查找完成。

該代理對象每次接收到NSMutableSet消息時(shí),會向mutableSetValueForKey:消息的接收對象發(fā)送一個(gè)set<Key>:消息。

注意:第3步中描述的機(jī)制比第1步的效率要低得多,因?yàn)樗赡苌婕爸貜?fù)創(chuàng)建新的集合對象而不是修改現(xiàn)有的集合對象。因此,在設(shè)計(jì)我們自己的兼容鍵值編碼的對象時(shí),通常應(yīng)該避免使用該機(jī)制。

  1. 如果未找到mutable set方法和訪問器方法,并且mutableSetValueForKey:消息的接收對象的類方法accessInstanceVariablesDirectly返回YES,則按順序依次查找名為_<key>或者<key>的實(shí)例變量。如果存在實(shí)例變量,則返回一個(gè)代理對象,查找完成。

該代理對象會將其接收到的所有NSMutableSet消息轉(zhuǎn)發(fā)給實(shí)例變量,該實(shí)例變量通常是NSMutableSet或其子類之一。

  1. 如果以上所有步驟都失敗,則返回一個(gè)代理對象,查找完成。

該代理對象在收到NSMutableSet的消息時(shí),向mutableSetValueForKey:消息的接收對象發(fā)送一個(gè)setValue:forUndefinedKey:消息。

實(shí)現(xiàn)基本的鍵值編碼兼容

當(dāng)對對象采用鍵值編碼時(shí),依賴于對象從NSObject類繼承的NSKeyValueCoding協(xié)議的默認(rèn)實(shí)現(xiàn)。反過來,默認(rèn)實(shí)現(xiàn)依賴于我們根據(jù)某些明確的格式來定義對象的實(shí)例變量和訪問器方法,以便在接收鍵值編碼消息時(shí),它可以將鍵字符串與屬性相關(guān)聯(lián)。

通常,通過使用@property語句聲明屬性并允許編譯器自動(dòng)合成實(shí)例變量(ivar)和訪問器來遵循Objective-C中的標(biāo)準(zhǔn)格式。默認(rèn)情況下,編譯器遵循預(yù)期的格式。

如果需要在Objective-C中手動(dòng)實(shí)現(xiàn)訪問器或?qū)嵗兞?,請遵循本?jié)中的指導(dǎo)原則來維護(hù)基本的兼容性。

基本的Getter

要實(shí)現(xiàn)返回屬性值的getter,同時(shí)可能還要執(zhí)行其他自定義工作,請使用名稱為該屬性名的方法,例如title字符串屬性:

- (NSString*)title
{
    // Extra getter logic…

    return _title;
}

對于保存布爾值的屬性,也可以使用前綴為is的方法,例如hidden布爾屬性:

- (BOOL)isHidden
{
    // Extra getter logic…

    return _hidden;
}

當(dāng)屬性是標(biāo)量或者結(jié)構(gòu)體時(shí),鍵值編碼的默認(rèn)實(shí)現(xiàn)將值包裝在對象中,以便在協(xié)議方法的接口上使用,如表示非對象值中所述,無需執(zhí)行任何特殊操作就可支持此行為。

基本的Setter

要實(shí)現(xiàn)存儲屬性值的setter,請使用名稱為帶有前綴set的首字母大寫的屬性名的方法。 對于hidden屬性:

- (void)setHidden:(BOOL)hidden
{
    // Extra setter logic…

    _hidden = hidden;
}

警告:不要在set<Key>:方法中調(diào)用驗(yàn)證屬性中描述的驗(yàn)證方法。

當(dāng)屬性是非對象類型(例如hidden布爾)時(shí),協(xié)議的默認(rèn)實(shí)現(xiàn)會檢測基礎(chǔ)數(shù)據(jù)類型,并在將其應(yīng)用于setter之前,解包傳遞給setValue:forKey:方法的對象值(在本例中是一個(gè)NSNumber實(shí)例),如表示非對象值中所述。但是,如果有可能將nil值寫入非對象屬性,則覆蓋setNilValueForKey:方法來處理這種情況,如處理非對象值中所述。hidden屬性的處理方式只是將nil解釋為NO

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

如果有必要,即使允許編譯器合成setter,我們也可以提供上述方法覆蓋。

實(shí)例變量

當(dāng)某個(gè)鍵值編碼訪問器方法(例如valueForKey:)的默認(rèn)實(shí)現(xiàn)無法查找到屬性的訪問器時(shí),它會詢問調(diào)用accessInstanceVariablesDirectly方法來詢問該類是否允許直接使用實(shí)例變量。默認(rèn)情況下,此類方法返回YES,但可以覆蓋此方法返回NO。

如果允許使用實(shí)例變量,請確保使用帶下劃線_前綴的屬性名來命名它們。通常情況下,編譯器在自動(dòng)合成屬性時(shí)會執(zhí)行此操作。但如果使用顯式的@synthesize指令,則可以手動(dòng)執(zhí)行這種命名操作。

@synthesize title = _title;

在某些情況下,會使用@dynamic指令告知編譯器將在運(yùn)行時(shí)提供getter和setter,而不是使用@synthesize或者允許編譯器自動(dòng)合成屬性??梢酝ㄟ^這樣做來避免自動(dòng)合成setter,以便可以提供集合訪問器,如定義集合方法中所述。在這種情況下,手動(dòng)聲明實(shí)例變量作為接口聲明的一部分。

@interface MyObject : NSObject {
    NSString* _title;
}

@property (nonatomic) NSString* title;

@end

定義集合方法

當(dāng)使用標(biāo)準(zhǔn)命名約定來創(chuàng)建訪問器和實(shí)例變量時(shí),鍵值編碼協(xié)議的默認(rèn)實(shí)現(xiàn)可以定位到它們以響應(yīng)鍵值編碼消息。對于表示to-many relationships(請看訪問對象屬性中的描述)的集合對象,情況和其他屬性一樣。但是,如果實(shí)現(xiàn)了集合訪問器,而不是集合屬性的基本訪問器,則可以:

  • NSArrayNSSet以外的集合類建立to-many relationships。在對象中實(shí)現(xiàn)集合方法時(shí),getter的默認(rèn)實(shí)現(xiàn)返回一個(gè)代理對象,代理對象會調(diào)用這些集合方法來響應(yīng)它接收到的NSArrayNSSet消息。底層集合屬性不必是NSArrayNSSet的實(shí)例,因?yàn)榇韺ο髸褂眠@些集合方法來提供預(yù)期的行為。
  • 在改變to-many relationships的內(nèi)容時(shí),提供更好的性能。協(xié)議的默認(rèn)實(shí)現(xiàn)會使用集合方法來改變屬性,而不是使用基本的setter重復(fù)創(chuàng)建新的集合對象來響應(yīng)每個(gè)更改。
  • 為對象的集合屬性的內(nèi)容提供鍵值觀察兼容的訪問。有關(guān)鍵值觀察的更多信息,請參看KVO。

可以實(shí)現(xiàn)兩類集合訪問器中的一種,具體取決于是希望關(guān)系的行為類似于一個(gè)索引的、有序的集合(如NSArray對象),還是一個(gè)無序的、唯一的集合(如NSSet對象)。在任何一種情況下,都要至少實(shí)現(xiàn)一組方法來支持對屬性的讀取訪問,然后額外添加一組方法來使集合的內(nèi)容的突變成為可能。

注意:鍵值編碼協(xié)議未聲明本節(jié)中描述的方法。取而代之的是,NSObject提供的協(xié)議的默認(rèn)實(shí)現(xiàn)會在兼容鍵值編碼的對象中查找這些方法,如訪問器查找方式中所述,并使用它們來處理鍵值編碼消息。

訪問索引集合

添加索引訪問器方法來提供一個(gè)計(jì)算、檢索、添加和替換有序關(guān)系中的對象的機(jī)制。底層集合屬性通常是NSArray或者NSMutableArray的實(shí)例,但是如果提供集合訪問器,則使得實(shí)現(xiàn)了這些方法的任何集合對象像數(shù)組一樣被操作成為了可能。

索引集合Getter

對于一個(gè)沒有默認(rèn)getter的集合屬性,如果提供以下索引集合getter方法,協(xié)議的默認(rèn)實(shí)現(xiàn)在響應(yīng)valueForKey:消息時(shí),會返回一個(gè)行為類似于NSArray的代理對象,但該代理對象調(diào)用這些集合方法來執(zhí)行其工作。

注意:在現(xiàn)代Objective-C中,編譯器默認(rèn)為每個(gè)屬性合成一個(gè)getter,因此默認(rèn)實(shí)現(xiàn)不會使用本節(jié)中的方法創(chuàng)建只讀代理(請查看Getter的查找方式)。 可以通過不聲明屬性(僅依賴于ivar)或?qū)傩月暶鳛?code>@dynamic(告知編譯器會在運(yùn)行時(shí)提供訪問器行為)來解決此問題。 無論哪種方式,編譯器都不會提供默認(rèn)的getter,并且默認(rèn)實(shí)現(xiàn)會使用以下方法。

  • countOf<Key>:此方法將to-many relationship中的對象數(shù)作為NSUInteger返回,就像數(shù)組的原始方法count一樣。實(shí)際上,當(dāng)?shù)讓蛹蠈傩允且粋€(gè)NSArray時(shí),使用count方法返回結(jié)果。
    例如,對于表示一個(gè)銀行交易列表的to-many relationship,由名為為transactionsNSArray支持:
- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}
  • objectIn<Key>AtIndex:或者<key>AtIndexes::第一個(gè)方法返回to-many relationship中在指定的索引位置的對象,而第二個(gè)方法返回在由NSIndexSet參數(shù)指定的索引位置的對象數(shù)組。 它們分別對應(yīng)于NSArray方法objectAtIndex:objectsAtIndexes:,只需要實(shí)現(xiàn)其中一個(gè)。 transactions數(shù)組的對應(yīng)應(yīng)方法為:
- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}

- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}
  • get<Key>:range::此方法是可選的,但可以提高性能。 它返回集合中處于指定范圍內(nèi)的對象,對應(yīng)于NSArray方法getObjects:range:transactions數(shù)組的實(shí)現(xiàn)為:
- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}

索引集合Mutator

支持有序可變的to-many relationship需要實(shí)現(xiàn)不同的方法組。當(dāng)提供這些setter方法時(shí),默認(rèn)實(shí)現(xiàn)在響應(yīng)mutableArrayValueForKey:消息時(shí),返回一個(gè)行為類似于NSMutableArray對象的代理對象,但該代理對象會使用mutableArrayValueForKey:消息的接收對象的方法來執(zhí)行其工作。這通常比直接返回NSMutableArray對象更有效,它還使得to-many relationship的內(nèi)容兼容鍵值觀察成為可能。

為了使對象的鍵值編碼兼容一個(gè)可變有序的to-many relationship,請實(shí)現(xiàn)以下方法:

  • insertObject:in<Key>AtIndex:或者insert<Key>:atIndexes::第一個(gè)方法接收要插入的對象和該對象的索引,第二個(gè)方法接收一個(gè)對象數(shù)組和包含對象數(shù)組中每個(gè)對象的索引的NSIndexSet對象,只需要其中一種方法。它們類似于NSMutableArrayinsertObject:atIndex:insertObjects:atIndexes:方法。
    transactions對象被聲明為一個(gè)NSMutableArray
- (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::第一個(gè)方法接收要?jiǎng)h除的對象的索引,第二個(gè)方法接收一個(gè)包含要?jiǎng)h除對象的索引的NSIndexSet對象,只需要其中一種方法。它們對應(yīng)于NSMutableArrayremoveObjectAtIndex:removeObjectsAtIndexes:方法。
- (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>::這些替換訪問器為代理對象提供了一種直接替換集合中的對象的方式,而不必刪除一個(gè)對象之后再插入一個(gè)對象。它們對應(yīng)于NSMutableArrayreplaceObjectAtIndex:withObject:replaceObjectsAtIndexes:withObjects:方法。
- (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];
}

訪問無序集合

添加無序集合訪問器方法,以提供一種訪問和修改無序關(guān)系中的對象的機(jī)制。通常,底層集合屬性是一個(gè)NSSet或者NSMutableSet實(shí)例。 但是,當(dāng)集合屬性實(shí)現(xiàn)了這些訪問器時(shí),使得對象像NSSet實(shí)例一樣操作成為了可能。

無序集合Getter

當(dāng)提供以下集合getter方法以返回集合中的對象數(shù)、迭代集合對象和測試對象是否已存在于集合中時(shí),響應(yīng)valueForKey消息的協(xié)議的默認(rèn)實(shí)現(xiàn)返回一個(gè)行為類似于NSSet的代理對象,但其調(diào)用以下集合方法來完成其工作。

  • countOf<Key>:此方法返回集合中的對象數(shù),對應(yīng)于NSSetcount方法。當(dāng)?shù)讓蛹蠈ο笫?code>NSSet時(shí),直接調(diào)用此方法。例如,名為employeesNSSet對象包含Employee對象:
- (NSUInteger)countOfEmployees {
    return [self.employees count];
}
- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}
  • memberOf<Key>::此方法將傳遞的對象與集合中的內(nèi)容進(jìn)行比較,并將匹配對象作為參數(shù)返回。如果未找到匹配的對象,則返回nil。如果手動(dòng)實(shí)現(xiàn),通常使用isEqual:方法來比較對象。 當(dāng)?shù)讓蛹蠈ο笫?code>NSSet對象時(shí),可以使用等效的member:方法。
- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}

無序集合Mutator

支持無序可變的to-many relationship需要實(shí)現(xiàn)不同的方法組。當(dāng)提供這些setter方法時(shí),默認(rèn)實(shí)現(xiàn)在響應(yīng)mutableSetValueForKey:消息時(shí),返回一個(gè)行為類似于NSMutableSet對象的代理對象,但該代理對象會使用mutableSetValueForKey:消息的接收對象的方法來執(zhí)行其工作。這通常比直接返回NSMutableSet對象更有效,它還使得to-many relationship的內(nèi)容兼容鍵值觀察成為可能。

為了使對象的鍵值編碼兼容一個(gè)可變無序的to-many relationship,請實(shí)現(xiàn)以下方法:

  • add<Key>Object:或者add<Key>::這些方法將一個(gè)或者一組對象添加到關(guān)系中,向關(guān)系添加一組項(xiàng)目時(shí),請確保關(guān)系中不存在同樣的對象。只需要其中一種方法,它們類似于NSMutableSetaddObject:unionSet:方法。對于employees集:
- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}

- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}
  • remove<Key>Object:或者remove<Key>::這些方法從關(guān)系中刪除單個(gè)或者一組項(xiàng)目,只需要其中一種方法。它們類似于NSMutableSetremoveObject:minusSet:方法。例如:
- (void)removeEmployeesObject:(Employee *)anObject {
    [self.employees removeObject:anObject];
}

- (void)removeEmployees:(NSSet *)manyObjects {
    [self.employees minusSet:manyObjects];
}
  • intersect<Key>::此方法接收一個(gè)NSSet參數(shù),從關(guān)系中刪除所有不是輸入集和集合集公共的對象。 這對應(yīng)于NSMutableSetintersectSet:。例如:
- (void)intersectEmployees:(NSSet *)otherObjects {
    return [self.employees intersectSet:otherObjects];
}

處理非對象值

通常情況下,兼容鍵值編碼的對象依賴于鍵值編碼的默認(rèn)實(shí)現(xiàn)來自動(dòng)包裝和解包非對象屬性,如表示非對象值中所述。但是,可以覆蓋此默認(rèn)行為。

如果兼容鍵值編碼的對象接收到一個(gè)將nil作為非對象屬性的值的setValue:forKey:消息,setValue:forKey:的默認(rèn)實(shí)現(xiàn)會發(fā)送一個(gè)setNilValueForKey:消息給該對象,該消息的默認(rèn)實(shí)現(xiàn)會引發(fā)一個(gè)NSInvalidArgumentException異常??梢愿采w該方法來提供特定的行為。

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

注意:當(dāng)一個(gè)對象覆蓋了不推薦使用的unableToSetNilForKey:方法時(shí),setValue:forKey:會調(diào)用該方法,而不是setNilValueForKey

添加驗(yàn)證

鍵值編碼協(xié)議定義了通過鍵或者鍵路徑來驗(yàn)證屬性的方法,這些方法的默認(rèn)實(shí)現(xiàn)依賴于我們定義的一些方法。具體來說,為任何想要驗(yàn)證的屬性提供一個(gè)validate<Key>:error:方法。默認(rèn)實(shí)現(xiàn)在響應(yīng)validateValue:forKey:error:消息時(shí)會查找這些方法。

如果沒有為屬性提供驗(yàn)證方法,則協(xié)議的默認(rèn)實(shí)現(xiàn)假定驗(yàn)證該屬性成功而不管屬性值是什么。

實(shí)現(xiàn)一個(gè)驗(yàn)證方法

當(dāng)為屬性提供一個(gè)驗(yàn)證方法時(shí),該方法通過引用接收兩個(gè)參數(shù):要驗(yàn)證的值對象和用于返回錯(cuò)誤信息的NSError。驗(yàn)證方法可以執(zhí)行以下三種操作之一:

  • 當(dāng)值對象有效時(shí),返回YES,并且不更改值對象和錯(cuò)誤。
  • 當(dāng)值對象無效且不能或不想提供有效的替代方法時(shí),請將error參數(shù)設(shè)置為NSError對象,該對象指示失敗的原因并返回NO。
  • 當(dāng)值對象無效但我們知道有效替代項(xiàng)時(shí),請創(chuàng)建有效對象,將值引用分配給新對象,并返回YES且不修改錯(cuò)誤引用。 如果提供其他值,則始終返回新對象,而不是修改正在驗(yàn)證的對象,即使原始對象是可變的。
- (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;
}

重要:在修改錯(cuò)誤引用之前,始終檢查錯(cuò)誤引用是否為NULL。

標(biāo)量值的驗(yàn)證

驗(yàn)證方法的value參數(shù)是一個(gè)對象,因此,非對象屬性的值包裝在NSNumber或者NSValue對象中。以下代碼演示了標(biāo)量屬性age的驗(yàn)證方法。

- (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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容