KVO和KVC的区别

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 本身又是一个类,它也有两个属性firstLine和lastLine, 那么我们如果想引用 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]
     }
   }

这次我们新添加了两个方法,countOfFriends和objectInFriendsAtIndex,这两个方法是 KVC 预定义的协议方法,用于集合类型的操作。注意这两个协议更明确的定义是这样countOf和objectInAtIndex。 其中的 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 肯定会对我们的开发有很大的帮助。关于 KVC 如果大家想了解更多,推荐大家看一看苹果官方的文档 Key-Value Coding Programming Guide。

你可能感兴趣的:(KVO和KVC的区别)