漫談 KVC 與 KVO

KVC 與 KVO 無疑是 Cocoa 提供給我們的一個非常強大的特性,使用熟練可以讓我們的代碼變得非常簡潔並且易讀。但 KVC 與 KVO 提供的 API 又是比較複雜的,絕對超出我們不經深究之前所理解到的複雜度,這次大家就來跟我一起深入認識這兩個特性吧。

基礎使用

首先,咱們要說的是 KVC (Key-Value Coding), 它是一種用間接方式訪問類的屬性的機制。在 Swift 中爲一個類實現 KVC 的話,需要讓它繼承自 NSObject:

class Person: NSObject {
    
    var firstName: String
    var lastName: String
    
    init(firstName: String, lastName: String) {
        
        self.firstName = firstName
        self.lastName = lastName
        
    }
    
}

這樣,我們就可以使用 KVC 的方式訪問 Person 類的屬性了:

let peter = Person(firstName: "Cook", lastName: "Peter")

print(peter.lastName)
print(peter.valueForKey("lastName")!)

注意我們的兩個 print 語句,第一個是使用直接引用屬性的方式,第二個就是使用 KVC 機制訪問的方式。 valueForKey 是 KVC 協議中定義的方法,它接受一個參數,我們把它叫做 key,這個 key 表示要訪問的屬性名稱,KVC 就會根據我們傳入的 key 幫助我們找到對應的屬性。

不同之處

在 Swift 中處理 KVC和 Objective-C 中還是有些細微的差別。比如,Objective-C 中所有的類都繼承自 NSObject,而 Swift 中卻不是,所以我們在 Swift 中需要顯式的聲明繼承自 NSObject。

可爲什麼要繼承自 NSObject 呢?我們在蘋果官方的 KVC 文檔中找到了答案。其實 KVC 機制是由一個協議 NSKeyValueCoding 定義的。NSObject 幫我們實現了這個協議,所以 KVC 核心的邏輯都在 NSObject 中,我們繼承 NSObject 才能讓我們的類獲得 KVC 的能力。(理論上說,如果你遵循 NSKeyValueCoding 協議的接口,其實也可以自己實現 KVC 的細節,完全行得通。但在實踐上,這麼做就不太值得了,太費時間了~)。

另外,因爲 Swift 中的 Optional 機制,所以 valueForKey 方法返回的是一個 Optional 值,我們還需要對返回值做一次解包處理,才能得到實際的屬性值。

關於 Optional 特性的內容,可以參考這兩篇文章
淺談 Swift 中的 Optionals
關於 Optional 的一點嘮叨

那麼書歸正傳,KVC 最主要的好處是什麼呢,簡單來說就是我們可以不用過多的依賴編譯時的限制,而是爲我們提供了更多的運行時的能力。

valueForUndefinedKey

還是繼續咱們上面的例子,假如我們又寫了這樣一個語句會怎麼樣呢:

peter.valueForKey("noExist")

因爲我們定義的 Person 類中是沒有 noExist 這個屬性的,所以 KVC 也無法找到這個屬性值,這時候 KVC 協議其實會調用 valueForUndefinedKey 方法,NSObject 對這個方法的默認實現是拋出一個 NSUndefinedKeyException 異常。所以如果我們沒有自己重寫 valueForUndefinedKey 方法的話,這時應用就會因爲異常崩潰。

我們也可以在 Person 類中實現我們自己的 valueForUndefinedKey 方法:

class PersonHandleUndefinedKey: NSObject {
    
    var firstName: String
    var lastName: String
    
    init(firstName: String, lastName: String) {
        
        self.firstName = firstName
        self.lastName = lastName
        
    }
    
    override func valueForUndefinedKey(key: String) -> AnyObject? {
        return ""
    }
    
}


let peter2 = PersonHandleUndefinedKey(firstName: "Cook", lastName: "Peter")
print(peter2.valueForKey("noExist"))

這次定義了 valueForUndefinedKey 對於未定義的 key 返回一個空字符串,這樣我們的 KVC 調用就能以更加優雅的方式處理這個異常行爲了。

valueForKeyPath

KVC 除了可以用單個的 key 來訪問單個屬性,還提供了一個叫做 keyPath 的東西。所謂 keyPath,就比如你的屬性本身也有自己的屬性,那麼想引用這個屬性,就需要用到 keyPath。咱們用一個示例來說明:


class Address: NSObject {
    
    var firstLine: String
    var secondLine: String
    
    init(firstLine: String, secondLine: String) {
        
        self.firstLine = firstLine
        self.secondLine = secondLine
        
    }
    
    
}

class PersonHandleKeyPath: NSObject {
    
    var firstName: String
    var lastName: String
    var address: Address
    
    init(firstName: String, lastName: String, address: Address) {
        
        self.firstName = firstName
        self.lastName = lastName
        self.address = address
        
    }
    
}


var peter3 = PersonHandleKeyPath(firstName: "Cook", lastName: "Peter", address: Address(firstLine: "Beijing", secondLine: "Haidian"))

print(peter3.valueForKeyPath("address.firstLine")!)

PersonHandleKeyPath 類定義了一個屬性 address, 這個 address 本身又是一個類,它也有兩個屬性 firstLinelastLine, 那麼我們如果想引用 address 的 firstLine 屬性,就可以使用 KVC 的 keyPath 機制:

print(peter3.valueForKeyPath("address.firstLine")!)

通過 keyPath,我們可以使用 KVC 將屬性引用範圍擴大很多。這個規則對 Cocoa 系統類也適用,比如:

let view = UIView()
print(view.valueForKeyPath("superview.superview"))

我們可以通過 KVC 的這個機制遍歷 UIView 層級。

同樣的,如果 keyPath 中引用的任何一級屬性不存在或者不符合 KVC 規範, valueForUndefinedKey 方法就會被調用。

SetValueForKey

KVC 定義了使用 valueForKey 方法獲取屬性的值,同樣也提供了設置屬性值的方法,就是 setValue:forKey ", 還是接着上面的例子:

peter3.setValue("swift", forKey: "firstName")
print(peter3.valueForKey("firstName")!)

setValue:forKey 方法接受兩個參數,第一個參數是我們要設置的屬性的值,第二個參數是屬性的 key。這個接口很簡單明瞭,就不多贅述了。

和 valueForKey 一樣,如果我們給 setValue 傳遞一個不存在的 key 值,KVC 就會去調用 setValue: forUndefinedKey 方法,NSObject 對這個方法的默認實現依然是拋出一個 NSUndefinedKeyException 異常。

關於標量值

所謂標量值(Scalar Type),指的是簡單類型的屬性,比如 int,float 這些非對象的屬性。關於標量值的在 KVC 中的處理有有些地方需要我們注意,我們把 Person 類再重寫一下:

class PersonForScalar : NSObject {
    
    var firstName: String
    var lastName: String
    var age: Int
    
    init(firstName: String, lastName: String, age: Int) {
        
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
        
    }
    
}

那麼現在可以使用 KVC 來操作它的各個屬性:

var person4 = PersonForScalar(firstName: "peter", lastName: "cook", age: 32)
person4.setValue(55, forKey: "age")
print(person4.valueForKey("age")!)

通過 setValue 方法,我們將 age 設置爲 55,並在下一行代碼中使用 valueForKey 將這個值打印出來。一切看似沒什麼不同。

那麼假如我們又寫了這一行語句呢:

person4.setValue(nil, forKey: "age")

額,你可以自己嘗試一下,這時候程序會崩潰。原因嘛,很簡單。 我們先來看 age 的定義:

var age: Int

age 是一個簡單標量值(Int 整型變量),而標量值是不能夠設置成 nil 的。雖然 KVC 提供給我們的 setValue 方法可以接受任何類型的參數作爲值的設置,但 age 的底層存儲確實標量值,因此我們執行上面那條 setValue 語句的時候必然會造成程序的崩潰。(這點在開發程序的時候確實需要格外留意,稍不留神可能就會浪費很多時間去調試錯誤)。

那麼我們除了注意避免將 nil 傳遞給底層存儲是標量類型的屬性之外,還有沒有其他方法呢? 答案是有的。

KVC 爲我們提供了一個 setNilValueForKey 方法,每當我們要將 nil 設置給一個 key 的時候,這個方法就會被調用,所以我們可以修改一下 Person 類的定義:

class PersonForScalar : NSObject {
    
    //...
    
    override func setNilValueForKey(key: String) {
        
        if key == "age" {
            
            self.setValue(18, forKey: "age")
            
        }
        
    }
    
    //...
    
}

我們在 setNilValueForKey 方法中,判斷如果當前的 key 是 age 的話,就給它設置一個默認值 18。這次我們再次傳入 nil 的時候,程序就不會因爲拋出異常而崩潰,而是爲這個 age 屬性設置一個默認值。

集合屬性

KVC 還提供了對集合屬性的處理,簡單來說就是這樣,我們爲 Person 類再添加一個 friends 屬性,用於表示這個人的朋友:

class PersonForCollection : NSObject {
    
    var firstName: String
    var lastName: String
    var friends: NSMutableArray
    
}

如果我們要爲某一個 Person 的實例添加一個新朋友,或者獲取它現有的朋友該怎麼做呢? 大家可能會直接想到這樣:

person5.friends.addObject(person6)

通過直接的屬性引用,我們可以完成這樣的需求。不過嘛,KVC 還給我們提供了專屬的集合操作協議,這樣我們就可以通過 KVC 的方式操作集合中的內容了,我們將 Person 類改寫一下:

class PersonForCollection : NSObject {
    
    var firstName: String
    var lastName: String
    var friends: NSMutableArray
    
    init(firstName: String, lastName: String) {
        
        self.firstName = firstName
        self.lastName = lastName
        self.friends = NSMutableArray()
        
    }

    func countOfFriends() -> Int {
        
        return self.friends.count
        
    }
    
    func objectInFriendsAtIndex(index: Int) -> AnyObject? {
        
        return self.friends[index]
        
    }
    
}

這次我們新添加了兩個方法,countOfFriendsobjectInFriendsAtIndex ,這兩個方法是 KVC 預定義的協議方法,用於集合類型的操作。注意這兩個協議更明確的定義是這樣 countOf<Key>objectIn<Key>AtIndex。 其中的 Key 代表集合操作的應的屬性 key 的名字。比如 countOfFriends, countOfAddress, countOfBooks 這些都是合法的集合操作協議方法,前提是隻要相應 key 值對應的屬性存在。

那麼集合操作方法定義好了,我們來看看如何使用 KVC 來操作集合屬性吧:

person5.mutableArrayValueForKey("friends").count

這個調用取得當前的 friends 集合的 count 屬性,這時候實際上調用了 countOfFriends 方法。自然,我們剛纔還實現了 objectInFriendsAtIndex 方法,大家也能推理出這個方法如何使用了吧:

let friend = person5.mutableArrayValueForKey("friends")[0]

就是這樣了,實際上 KVC 對於我們這個集合屬性 friends 的操作都會通過 mutableArrayValueForKey 方法來進行,它會用我們傳入的 key 值在當前實例中進行解析,如果接續成功會返回一個 NSMutableArray 類型的對象,我們就可以直接使用 NSMutableArray 的接口對集合類的屬性進行操作了,不論他的底層存儲是不是 NSMutableArray,它也是 NSKeyValueCoding 協議中定義的方法(這個協議定義我們在前面提到過,大家還記得吧~)。

我們剛纔實現了集合相關的兩個方法還缺了些什麼呢 — 我們只實現了集合操作的 getter 方法,並沒有實現 setter 方法。到目前,我們還不能通過 KVC 機制來給 firends 數組添加元素。

我們還需要添加兩個方法:

class PersonForCollection : NSObject {

    func insertObjectInFriendsAtIndex(friend: PersonForCollection, index: Int) {
        
        self.friends.insertObject(friend, atIndex: index)
        
    }
    
    func removeObjectFromFriendsAtIndex(index: Int) {
        
        self.friends.removeObjectAtIndex(index)
        
    }

}

insertObjectInFriendsAtIndexremoveObjectFromFriendsAtIndex 分別用於向 friends 屬性中插入元素和刪除元素。現在我們也可以用 KVC 來操作集合內容了:

person5.mutableArrayValueForKey("friends").addObject(person6)
person5.mutableArrayValueForKey("friends").count
person5.mutableArrayValueForKey("friends").removeObjectAtIndex(0)

通過 KVC 的集合操作協議,我們實現了直接用 KVC 接口來操作集合屬性的內容。 KVC 集合操作會更加靈活,friends 屬性不一定是 NSMutableArray 類型, 它的底層存儲可以是任何形式,只要我們實現了 KVC 集合操作接口,我們就能通過 KVC 像使用 NSMutableArray 一樣來操作底層的集合了。

總結

好了,關於 KVC 咱們就說這麼多,它還提供了很多其他非常好的特性,比如屬性驗證,可以通過這個方式來對屬性的設置過程進行類似 filter 的操作。還提供了keyPath 的集合操作,比如我們通過這樣一個 KeyPath 就可以獲得 friends 集合的元素總數:

person5.valueForKeyPath("friends.@count")

善用 KVC 肯定會對我們的開發有很大的幫助。關於 KVC 如果大家想了解更多,推薦大家看一看蘋果官方的文檔 Key-Value Coding Programming Guide

希望本篇文章的內容讓大家再看了之後多多少少有些收貨吧,我們下篇文章將會和大家一起探討 KVO 的相關內容,也希望大家喜歡。

本篇內容相關代碼的 playground 大家可以在 Github 上面找到: https://github.com/swiftcafex/kvc-kvo-samples

更多精彩內容可關注微信公衆號:
swift-cafe

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