漫谈 KVC 与 KVO

作者: SwiftCafe | 来源:发表于2016-01-05 20:43 被阅读1416次

    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

    相关文章

      网友评论

      • 加菲猫爱我:还有kvo在哪里?
      • 加菲猫爱我:如果在开篇先提醒一下读者kvc是基于一个约定(Property Accessor Naming Conventions),你必须首先遵循这个约定才能发挥kvc的作用,那就更好了!

      本文标题:漫谈 KVC 与 KVO

      本文链接:https://www.haomeiwen.com/subject/ikyzhttx.html