簡單易懂KVC基礎篇

博客日誌:2019-3-14 起筆。
博客日誌:2019-3-22 根據我的閱讀感覺對文章大幅重構。
博客日誌:2019-3-25 封筆。html

引言

這篇文章其實就是被他的兄弟KVO給逼出來的,沒辦法。官方文檔中介紹過KVC是KVO技術實現的基礎,閒話免提,我們請入座。學識有限,有不對的地方,還請你們多多指正。編程

概述

KVC(Key-value coding)鍵值編碼是一種由NSKeyValueCoding非正式協議(其實就是咱們所說的分類或類別)啓用的機制,對象採用該機制提供對其屬性間接訪問。當對象符合鍵值編碼時,其屬性可以使用字符串參數經過簡潔,統一的消息傳遞接口(方法)尋址。這種間接訪問機制補充了實例變量及其相關訪問器方法提供的直接訪問。數組

鍵值編碼是一個基本概念,是許多其餘Cocoa技術的基礎,例如KVO,(macOS)Cocoa綁定,Core Data和AppleScript。在某些狀況下,鍵值編碼還有助於簡化代碼。app

這裏咱們搞了段很官方的描述,其實簡單來講的話,就是經過字符串名稱訪問對象屬性,就這麼簡單。性能

API接口

普通用法

訪問對象屬性

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
複製代碼

KVC提供了簡潔,統一的方法,用來訪問對象屬性。分別是對應於getter訪問器的valueForKey:和對應於setter訪問器的setValue:forKey:。幸運的是,NSObject採用了NSKeyValueCoding協議併爲它們和其餘基本方法提供默認實現。所以,若是你從NSObject(或其許多子類中的任何一個)派生對象,那麼大部分都工做已經完成了。學習

@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 Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@end
複製代碼

如今咱們聲明瞭兩個類來講明KVC的基礎用法,咱們假設BankAccount的實例對象是myAccount,一般咱們會直接使用訪問器方法操做屬性。測試

myAccount.currentBalance = @(100.0);
// 或者
[myAccount setCurrentBalance:@(100.0)];
複製代碼

固然咱們知道上面兩個方法是等價的。如今咱們看一下KVC的使用方式:優化

// setter
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
// getter
NSNumber *currentBalance = [myAccount valueForKey:@"currentBalance"];
複製代碼

按鍵路徑訪問屬性

若是咱們想要獲取銀行帳戶戶主的姓名,咱們能夠在引入Person.h以後,使用點語法很輕鬆的獲取到:ui

NSString *myName = myAccount.owner.name;
複製代碼

固然KVC也提供了咱們訪問屬性的屬性的操做方法,經過鍵路徑來訪問屬性。鍵路徑是以點分隔多個鍵的字符串用來指定要遍歷的對象屬性的序列。序列中第一個鍵是相對於接收者的屬性,而且每一個後續鍵是相對於前一個鍵的屬性。編碼

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
複製代碼

如今咱們可使用鍵路徑訪問屬性了:

NSString *myName = [myAccount valueForKeyPath:@"owner.name"];
[myAccount setValue:@"SepCode" forKeyPath:@"owner.name"];
複製代碼

鍵未定義異常

根據KVC規定的方式(搜索模式)找不到由key命名的屬性時,就會調用獲取值的valueForUndefinedKey:或設置值的setValue:forUndefinedKey:方法,系統默認的該方法會引起一個 NSUndefinedKeyException的異常致使崩潰,咱們能夠重寫該方法避免崩潰。而且咱們也能夠在重寫該方法時,加入邏輯處理以使其更加的優雅。

// 重寫UndefinedKey:方法
// getter
- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
// setter
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
}
複製代碼

非對象值和nil

當咱們經過setValue:forKey:對屬性賦值,若是該屬性不是對象而是標量或結構體時,KVC會自動展開對象獲取值並賦值給屬性。一樣當執行valueForKey:時,則會自動包裝屬性值,返回一個與其對應的NSNumber或NSValue對象。

// setter
[owner setValue:@(26) forKey:@"age"];
// getter
NSNumber *myAge = [owner valueForKey:@"age"];
複製代碼

當咱們給對象賦值nil時,這很容易理解,表示把對象設置爲空。可是當咱們經過setValue:forKey:設置非對象屬性值爲nil時,沒有對象可展開了,難道咱們都把這些非對象值設置爲0嗎?官方並無給咱們實現默認的賦值操做,而是調用setNilValueForKey:方法,而系統默認的該方法會引起一個NSInvalidArgumentException的異常,固然咱們也能夠重寫該方法實現特定的行爲。

// nil
[owner setValue:nil forKey:@"age"];
...
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@」age」];
    } else {
        [super setNilValueForKey:key];
    }
}
複製代碼

多值訪問

咱們看到官方還提供了dictionary相關的方法,但他並非針對字典的方法。而是同時訪問多個屬性的方法,其實就是調用每一個key的setValue:forKey:valueForKey:方法,這很容易理解咱們再也不贅述。

NSDictionary *dict = [owner dictionaryWithValuesForKeys:@[@"name",@"age"]];
dict = @{@"name":@"sepCode",@"age":@(62)};
[owner setValuesForKeysWithDictionary:dict];
複製代碼

特殊用法

訪問集合屬性

咱們前面講述了KVC訪問對象的方式,固然它也一樣適用於集合對象。你能夠像使用任何其餘對象同樣,經過valueForKey:setValue:forKey:(或它們的鍵路徑方式)獲取或設置集合對象。

@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end
複製代碼

如今咱們又定義了一個交易類,假如咱們想獲取我的銀行帳戶中的全部收款人。

NSArray *payees = [myAccount valueForKeyPath:@"transactions.payee"];
複製代碼

請求transactions.payee鍵路徑的值將返回一個數組,包含transactions中全部的payee對象。這也適用於鍵路徑中的多個數組。假如咱們想獲取多個銀行帳戶中的全部收款人,請求鍵路徑accounts.transactions.payee的值返回一個數組,其中包含全部賬戶中全部交易的全部收款人對象。

對於獲取值咱們看到了KVC的方便之處,可是對於設置值咱們卻不多用到KVC。它會把集合內包含的全部鍵對象的值設置爲相同的值,這不是咱們想要的結果。

雖然咱們可使用通用的方式訪問集合對象,可是,當你想要操縱這些集合的內容時,官方推薦咱們最有效的方法是使用協議定義的可變代理方法。 協議爲訪問集合對象定義了三種不一樣的代理方法,每種方法都有key和keyPath變種: mutableArrayValueForKey:mutableArrayValueForKeyPath: 它們返回一個行爲相似於NSMutableArray對象的代理對象。 mutableSetValueForKey:mutableSetValueForKeyPath: 它們返回一個行爲相似於NSMutableSet對象的代理對象。 mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 它們返回一個行爲相似於NSMutableOrderedSet對象的代理對象。

當你對代理對象進行操做,添加對象,從中刪除對象或替換對象時,協議的默認實現會相應地修改原對象。如今假如咱們想使用KVC通用方法,在我的銀行帳戶增長一次交易,經過valueForKey:獲取非可變集合對象,建立可變集合對象增長內容,而後使用setValue:forKey:消息將其存儲回對象。相比之下經過代理對象操做,就顯得方便不少。在許多狀況下,它比直接使用可變屬性更有效。例如,當咱們不使用常量字符串做爲key,而是使用變量時。這容許咱們沒必要知道調用方法的確切名稱,只要對象和正在使用的key符合KVC,一切都會正常工做。 當維護集合中對象時,這些方法還使其能夠支持鍵值觀察機制。這也是爲何KVO的文章寫到一半時,我又忽然先來寫KVC了。

這裏咱們須要注意的是,這些方法的做用是返回一個集合對象的代理對象。固然你也能夠像咱們以前講到的同樣,請求集合內對象的屬性,從而達到返回一個屬性集合對象,但這僅僅侷限於獲取值。若是這種狀況下操做屬性集合對象原集合內的對象的屬性的值就會被設置爲操做後的屬性集合對象,這也不是咱們想要的結果。

使用集合運算符

當你向符合鍵值編碼的對象發送valueForKeyPath:消息時,或者表述爲當對象調用valueForKeyPath:方法時,能夠在鍵路徑中嵌入集合運算符。集合運算符是一個前面是at符號(@)的關鍵字,它指定了getter應該執行的操做,以便在返回以前以某種方式操做數據。NSObject爲此行爲提供了默認實現。

當鍵路徑包含集合運算符時,運算符以前的鍵路徑(稱爲左鍵路徑)指示相對於消息接收者操做的集合。若是將消息直接發送到集合對象(例如NSArray實例),則能夠省略左鍵路徑。操做符以後的鍵路徑部分(稱爲右鍵路徑)指定操做員應處理的集合中的屬性。除了@count以外,全部集合運算符都須要右鍵路徑。

集合運算符鍵路徑格式

集合運算符的表現行爲可分爲三種基本類型:

  • 聚合運算符以某種方式合併集合的對象,並返回一般與右鍵路徑中指定的屬性的數據類型匹配的單個對象。@count是一個例外,它沒有右鍵路徑即使是有也會被忽略並始終將返回一個NSNumber實例。

    NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
    NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
    NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
    複製代碼
  • 數組運算符返回與右鍵路徑指示的特定對象集相對應的對象數組。

  • 嵌套操做符處理包含其餘集合的集合,並根據操做符返回一個NSArray或NSSet實例,它以某種方式組合嵌套集合的對象。

具體運算符用法,請點擊上述各種型超連接在官方文檔中查看。

屬性驗證

鍵值編碼協議定義了支持屬性驗證的方法。就像使用KVC通用方法同樣,你也能夠按鍵(或鍵路徑)驗證屬性。當你調用validateValue:forKey:error:(或validateValue:forKeyPath:error:)方法時,協議的默認實現會使對象實例搜索是否實現了validate<Key>:error:方法。若是對象沒有實現此類方法,則默認驗證成功,並返回YES。

一般可採用如下驗證方式:

  • 當值對象有效時,返回YES,不更改值對象或錯誤。

  • 當值對象無效時,而且你不能或不想提供有效的替代方法,設置錯誤緣由NSError而且返回NO。

  • 當值對象無效但你知道有效的替代方法時,建立有效對象,將值引用分配給新對象,而後返回YES,不設置NSError錯誤。若是提供其餘值,則始終返回新對象,而不是修改正在驗證的對象,即便原始對象是可變的。

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

- (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;
}
複製代碼

上述用例演示了一個name字符串屬性的驗證方法,該方法確保值對象的最小長度和不爲nil。若是驗證失敗,此方法不會替換其餘值。

原理解析

訪問者搜索模式

KVC協議中最關鍵的部分就是訪問者搜索模式,NSObject提供的NSKeyValueCoding協議的默認實現,使用明肯定義的規則集將基於鍵的訪問器(KVC存取方法)調用映射到對象的屬性。這些協議方法使用鍵參數在其本身的對象實例中搜索訪問器,實例變量以及遵循某些命名約定的相關方法。

可變數組的搜索模式

這裏咱們僅介紹一種模式可變數組的搜索模式,其餘搜索模式可經過訪問者搜索模式瞭解詳細內容。

mutableArrayValueForKey:的默認實現,輸入一個鍵參數,返回一個可變代理數組。對象內部的名爲key的屬性,經過如下過程接受訪問器的調用:

  1. 查找一對方法名如insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(分別對應於NSMutableArray的基本方法insertObject:atIndex:removeObjectAtIndex:)或名稱相似於insert<Key>:atIndexes:remove<Key>AtIndexes:的方法(對應於NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:方法)。

    若是對象具備至少一個插入方法和至少一個刪除方法,返回一個代理對象來響應這些NSMutableArray的消息。經過發送一些組合的消息insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:,和remove<Key>AtIndexes:mutableArrayValueForKey:消息的接受者來實現。 或者能夠表述爲經過使調用mutableArrayValueForKey:方法的對象,調用上述方法,來響應這些插入或刪除方法。

    當接收mutableArrayValueForKey:消息的對象也實現名稱爲replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:的(可選)替換方法時,代理對象也會在適當時使用這些方法以得到最佳性能。

  2. 若是對象沒有可變數組的方法,查找名稱與模式集匹配的set<Key>:的訪問器方法。在這種狀況下,返回一個代理對象。經過向mutableArrayValueForKey:的原始接收者發出set<Key>:消息,來響應上述那些NSMutableArray的消息。

    注意:前兩步簡單來講就是代理對象操做集合內容時,先去查找是否實現了插入,刪除,(可選)替換的方法,沒實現就去查找setter方法。步驟2中描述的機制比前一步驟的效率低得多,由於它可能涉及重複建立新的集合對象而不是修改現有的集合對象。所以,在設計本身的符合鍵值編碼的對象時,一般應該避免使用它。

  3. 若是既未找到可變數組方法,也未找到訪問器,而且接收者的類對accessInstanceVariablesDirectly的響應爲YES,表示容許搜索實例變量,則按順序搜索名稱爲_<key><key>的實例變量。 若是找到這樣的實例變量,則返回一個代理對象,該對象將它接收的每一個NSMutableArray消息轉發給實例變量,一般是NSMutableArray或其子類之一的實例。

  4. 若是全部其餘方法都失敗了,則返回一個可變集合代理對象,該對象在收到NSMutableArray消息時向mutableArrayValueForKey:消息的原始接收者發出setValue:forUndefinedKey:消息。 setValue:forUndefinedKey:的默認實現會引起NSUndefinedKeyException異常。

    注意:後兩步簡單來講就是,若是容許搜索實例變量,就去查找變量,若是以上搜索都失敗,就報錯。

原理實踐

如今咱們根據可變數組的搜索模式,作一些實踐和測試:

@interface ViewController ()
/// array
@property (nonatomic, strong) NSMutableArray *array;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    self.array = [@[@(1),@(2),@(3)] mutableCopy];
    NSMutableArray *kvcArray = [self mutableArrayValueForKey:@"array"];
    // 發送NSMutableArray消息
    [kvcArray addObject:@(4)];
    [kvcArray removeLastObject];
    [kvcArray replaceObjectAtIndex:0 withObject:@(4)];
    
}
// 可變數組多對多優化
- (void)insertObject:(NSNumber *)object inArrayAtIndex:(NSUInteger)index {
    [self.array insertObject:object atIndex:index];
}

- (void)removeObjectFromArrayAtIndex:(NSUInteger)index {
    [self.array removeObjectAtIndex:index];
}

- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object {
    [self.array replaceObjectAtIndex:index withObject:object];
}

@end
複製代碼

上圖的測試結果,向咱們展示了若是咱們使用代理對象時,最好實現完整協議,優化多對多關係,不然隨着數據量級增長,性能會呈指數級降低,這真的很糟糕。

疑點解惑

在這裏我要說一下我對於kvc是kvo實現的基礎的理解。由於在網上看到一位文章寫的還不錯的做者,他講到兩者實現機制不一樣,並沒有必然聯繫,只是KVC對KVO的支持比較好。我很是不一樣意這個觀點。在官方鍵值觀察編程指南中明確指出,該類的屬性必須遵照KVC合規性。KVC是一個經過字符串訪問對象屬性的協議,包括搜索模式也屬於該協議的一部分。KVO觀察的屬性,必須遵照KVC合規性,而且支持觀察KVC兼容的全部訪問器修改屬性。一般咱們所理解的KVO都是基於setter訪問器實現的,然而並不是如此。下圖也充分驗證KVO支持KVC的搜索模式:

這裏讓我想到了餓了麼技術沙龍中蘭建剛的忠告:中文博客-在你沒有能力分辨對錯以前,少看。

結語

這篇文章呢,寫着寫着我就又有感慨了。我深深的感覺到,我是一個學習者,這些知識都是別人創造的,用的都是別人提供給咱們的方法。就連學習也多是靠他人總結的,我還不是一個創造者。

不過認清本身是多麼菜,也沒什麼很差的。即使一樣處於學習階段的他人,也能夠成爲本身的老師,但願你們能夠多多指點迷津。

最近看了很多他人的文章,我從本身的感覺發現幾點。

  • 喜歡做者把技術經過圖或者文字表述的很清楚,不喜歡看做者大段的代碼來表述,可是簡單的用例仍是必須的。
  • 不要一會兒把接口全列出來,最多掃一眼,除非做者的目的也是你就瞄一眼就能夠了。因此講解時的順序能夠是表述,接口,用例。一個點一個點的展開。
  • 文章結構清晰,不要天上一腳,地上一腳,因此前提是做者思路清晰。

另外有大牛建議不須要看太多書,經典的書多讀幾遍,獨立思考。本篇文章基本是在多看官方文檔的基礎上誕生的,本人對於細節知識仍是比較在乎的,若是有理解不對的地方,還請你們多多指正。

相關文章
相關標籤/搜索