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<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 肯定会对我们的开发有很大的帮助。关于 KVC 如果大家想了解更多,推荐大家看一看苹果官方的文档 Key-Value Coding Programming Guide

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    黑暗中的孤影阅读 49,367评论 74 440
  • KVC 与 KVO 无疑是 Cocoa 提供给我们的一个非常强大的特性,使用熟练可以让我们的代码变得非常简洁并且易...
    SwiftCafe阅读 1,853评论 3 14
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    朽木自雕也阅读 1,404评论 6 1
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    Fendouzhe阅读 560评论 0 6
  • 文 沁蓝 这是一个没有投资就没有进步的社会。 这是一个投资激发爆炸式增长的时代。 投资让我们的生活更美好,我们用的...
    沁蓝说阅读 405评论 2 0