漫谈 KVC 与 KVO

swift 发布于 2018年07月20日
基础使用

首先,咱们要说的是 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,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe