zoukankan      html  css  js  c++  java
  • 漫谈 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

  • 相关阅读:
    hdu 1028 Ignatius and the Princess III (n的划分)
    CodeForces
    poj 3254 Corn Fields (状压DP入门)
    HYSBZ 1040 骑士 (基环外向树DP)
    PAT 1071 Speech Patterns (25)
    PAT 1077 Kuchiguse (20)
    PAT 1043 Is It a Binary Search Tree (25)
    PAT 1053 Path of Equal Weight (30)
    c++ 常用标准库
    常见数学问题
  • 原文地址:https://www.cnblogs.com/theswiftworld/p/kvc.html
Copyright © 2011-2022 走看看