Key-Value Coding Programming Guide

536 阅读53分钟

Key-Value Coding Programming Guide

由于文章过长, 所以把总结放到开头

总结

  1. setValueForKey:, valueForKey:都是先查找对应的settergetter然后再查找实例变量
  2. setValueForKeyPath:setValueForKey:的多步实现, valueForKeyPath:valueForKey:的多步实现
  3. 集合的(如NSArray,NSMUtableArray等)的setValueForKey:, valueForKey:的搜索步骤会更多些, 会产生个中间代理proxy集合, 会查询一系列的集合方法(可查看GUNStep的源码)
  4. 非对象类型(标量, 结构体属性)的装包和解包
  5. 按照需要为属性提供validate<Key>:error:方法
  6. 原文很多长句, 翻译的有点涩, 结合代码会好理解很多

Getting Started

About Key-Value Coding

键值编码是NSKeyValueCoding非正式协议支持的一种机制, 对象采用这种机制来提供对其属性的间接访问. 当对象符合键值编码时, 可通过简洁, 统一的消息传递接口通过字符串参数访问其属性. 这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问.

通常, 使用访问器方法来访问对象的属性. get访问器(或getter)返回属性的值. set访问器(或setter)设置属性的值. 在Objective-C中还可以直接访问属性的底层实例变量. 以任何一种方式访问​​对象属性都很简单, 但是需要调用特定于属性的方法或变量名. 随着属性列表的增加或更改访问这些属性的代码也必须修改. 符合键值编码的对象提供了一个简单的消息传递接口, 该接口在其所有属性之间都是一致的. 键值编码是许多其他Cocoa技术的基础概念, 例如键值观察, Cocoa绑定,Core DataAppleScript-ability. 在某些情况下键值编码还可以帮助简化代码.

Using Key-Value Coding Compliant Objects

当对象继承(直接或间接)自NSObject时, 它们通常采用键值编码, 该对象既采用NSKeyValueCoding协议又为基本方法提供默认实现. 这样的对象通过紧凑的消息传递接口使其他对象可以执行以下操作

  • 访问对象属性
    • 该协议指定了一些方法例如通用的getter, valueForKey:和通用的setter, setValue:forKey:用于通过名称或键(参数为字符串)来访问对象属性. 这些方法和相关方法的默认实现使用key来定位底层数据并与之交互
  • 操作集合属性
    • 访问方法的默认实现与对象的集合属性(例如NSArray对象)一起使用, 就像其他任何属性一样. 此外如果对象为属性定义了集合访问器方法, 则它将启用对集合内容的键值访问. 这通常比直接访问更有效, 并且允许通过标准化接口使用自定义集合对象
  • 在集合对象上调用集合运算符
    • 在键值编码兼容对象中访问集合属性时, 可以将集合运算符插入键字符串中. 集合运算符指示默认的NSKeyValueCoding getter实现对集合执行操作, 然后返回新的, 经过过滤的集合版本或表示集合某些特征的单个值
  • 访问非对象属性
    • 协议的默认实现检测非对象属性(包括标量和结构体, )并自动将它们包装和解包装为对象, 以在协议接口上使用. 另外该协议声明了一种方法, 该方法允许兼容对象为通过键值编码接口在非对象属性上设置nil值的情况提供适当的操作
  • 通过key path访问属性
    • 当具有与键值编码兼容的对象的层次结构时, 可以使用基于key path的方法调用来通过单个调用在层次结构中深入以获取或设置值

Adopting Key-Value Coding for an Object

为了自己的对象符合键值编码, 请确保它们采用NSKeyValueCoding非正式协议并实现相应的方法. 例如valueForKey:作为通用gettersetValue:forKey:作为通用setter. 幸运的是如上所述, NSObject采用了该协议, 并为这些和其他必要方法提供了默认实现. 因此如果从NSObject(或其许多子类中的任何一个)派生对象, 则许多工作已经为我们完成了.

为了使默认方法能够正常工作, 请确保对象的访问器方法和实例变量遵循某些明确定义的模式. 这些允许默认实现根据键值编码的消息查找对象的属性. 然后可以选择通过提供验证方法和处理某些特殊情况的方法来扩展和自定义键值编码

Key-Value Coding with Swift

从NSObject或其子类之一继承的Swift对象默认情况下对其属性遵循键值编码. 在Objective-C中, 属性的访问器和实例变量必须遵循某些模式, 而Swift中的标准属性声明会自动保证这一点. 另一方面, 许多协议的功能要么不相关, 要么可以使用在Objective-C中没有的原生Swift构造或技术更好地处理. 例如由于所有Swift属性都是对象, 因此永远不会对 对非对象属性默认实现的特殊处理

因此, 尽管键值编码协议方法可以直接转换为Swift. 但本指南主要关注Objective-C, 在此需要做更多工作以确保合规性, 并且键值编码通常是最有用的. 整个指南中都指出了需要在Swift中采用截然不同的方法的情况

Other Cocoa Technologies Rely on Key-Value Coding

符合键值编码的对象可以参与各种依赖于这种访问的Cocoa技术包括:

  • 键值观察
  • Cocoa绑定
    • 这一系列技术完全实现了"模型-视图-控制器"范例. 其中模型封装了应用程序数据. 视图显示和编辑该数据,并且控制器在两者之​​间进行中介
    • 查看Cocoa Bindings Programming Topics
  • Core Data
    • 该框架为与对象生命周期和对象图管理相关的常见任务(包括持久性)提供了通用的自动化解决方案
    • 查看Core Data Programming Guide
  • AppleScript
    • 这种脚本语言可以直接控制可编写脚本的应用程序以及macOS的许多部分. Cocoa的脚本支持利用键值编码来获取和设置可脚本对象中的信息. NSScriptKeyValueCoding非正式协议中的方法提供了用于键值编码的其他功能, 包括通过在多值键中的索引获取和设置键值以及将键值强制(或转换)为适当的数据类型
    • 查看 AppleScript Overview

Key-Value Coding Fundamentals

Accessing Object Properties

对象通常在其接口声明中指定属性, 并且这些属性属于以下几类之一:

  • 单一属性
    • 这些是简单的值. 例如标量, 字符串或布尔值, 值对象(例如NSNumber)和其他不可变类型(例如NSColor)也被视为属性
  • 一对一关系
    • 这些是具有自己属性的可变对象. 对象的属性可以更改, 而无需更改对象本身. 例如银行帐户对象可能具有所有者owner属性, 该属性是Person对象的实例, 而Person对象本身具有address属性. 所有者的地址可以更改, 而无需更改银行帐户持有的所有者, 银行帐户的所有者未更改。只有他们的地址发生了更改
  • 一对多关系
    • 这些是集合对象. 尽管也可以使用自定义集合类, 但通常使用NSArrayNSSet的实例来保存此类集合

示例 BankAccount对象的属性

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
 
@end

为了维护封装对象通常为其接口上的属性提供访问器方法. 对象的作者可以显式地编写这些方法也可以依靠编译器自动合成它们. 无论哪种方式使用这些访问器之一的代码作者都必须在编译属性名称之前将其写入代码, 访问器方法的名称成为使用它的代码的静态部分. 例如在给定示例代码中声明的银行帐户对象, 编译器将合成一个可以为myAccount实例调用的设置器

[myAccount setCurrentBalance:@(100.0)];

这很直接但缺乏灵活性. 另一方面符合键值编码的对象提供了一种更通用的机制可以使用字符串标识符访问对象的属性

Identifying an Object’s Properties with Keys and Key Paths

key是标识特定属性的字符串. 通常按照约定, 代表属性的键是该属性本身在代码中出现的名称. key必须使用ASCII编码, 不能包含空格并且通常以小写字母开头(尽管有例外, 例如在许多类中找​​到的URL属性)

由于示例中的BankAccount类符合键值编码因此它可以识别键owner,currentBalancetransactions,它们是其属性的名称. 可以通过其键设置值而不是调用setCurrentBalance:方法

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

实际上可以使用相同的方法, 使用不同的关键参数来设置myAccount对象的所有属性, 因为参数是字符串, 所以它可以是在运行时进行操作的变量

key path是一串点分隔的key,用于指定要遍历的对象属性序列。序列中第一个键的属性是相对于接收者的,并且每个后续键都是相对于前一个属性的值进行评估的. Key paths对于通过单个方法调用向下钻取到对象层次结构很有用.

例如假定PersonAddress类也符合键值编码, 则应用于银行帐户实例的key path: owner.address.street指的是存储在银行帐户所有者地址中的街道字符串的值

注意Swift中可以使用#keyPath表达式来代替使用字符串来表示KeyKey Path. 这提供了编译时检查的优势. 如将SwiftCocoaObjective-C结合使用(Swift 3)指南的Keys and Key Paths部分所述

gnustep-base-1.25.0版本的- (void) setValue: (id)anObject forKeyPath: (NSString*)aKey实现(NSKeyValueCodingm line:374)

GNUstep Base1.25.0已是旧版, 点击下载查看新版

- (void) setValue: (id)anObject forKeyPath: (NSString*)aKey
{
  NSRange       r = [aKey rangeOfString: @"." options: NSLiteralSearch];
#ifdef WANT_DEPRECATED_KVC_COMPAT
  IMP	        o = [self methodForSelector: @selector(takeValue:forKeyPath:)];

  setupCompat();
  if (o != takePath && o != takePathKVO)
    {
      (*o)(self, @selector(takeValue:forKeyPath:), anObject, aKey);
      return;
    }
#endif

  if (r.length == 0)
    {
      [self setValue: anObject forKey: aKey];
    }
  else
    {
      // 找到key path最前面的`key`
      NSString	*key = [aKey substringToIndex: r.location];
      // 前面的key截断, 进如递归做准备
      NSString	*path = [aKey substringFromIndex: NSMaxRange(r)];
      
      // 然后递归进入下一个(key path的下一个key)
      [[self valueForKey: key] setValue: anObject forKeyPath: path];
    }
}

gnustep-base-1.25.0版本的- (id) valueForKeyPath: (NSString*)aKey实现(NSKeyValueCoding.m line:533)

- (id) valueForKeyPath: (NSString*)aKey
{
  NSRange       r = [aKey rangeOfString: @"." options: NSLiteralSearch];

  if (r.length == 0)
    {
      return [self valueForKey: aKey];
    }
  else
    {
      NSString	*key = [aKey substringToIndex: r.location];
      NSString	*path = [aKey substringFromIndex: NSMaxRange(r)];

      return [[self valueForKey: key] valueForKeyPath: path];
    }
}

Getting Attribute Values Using Keys

当对象采用NSKeyValueCoding协议时它符合键值编码, 继承自NSObject的对象(提供了该协议的基本方法的默认实现)会自动采用具有某些默认行为的该协议. 这样的对象至少实现以下基本的基于键的getter

  • valueForKey:
    • 返回由key参数命名的属性的值. 如果根据访问者搜索模式中所述的规则找不到由键命名的属性, 然后对象将向其自身发送valueForUndefinedKey:消息. valueForUndefinedKey:的默认实现抛出NSUndefinedKeyException异常, 但是子类可以重写此行为并更优雅地处理该情况
  • valueForKeyPath:
    • 返回相对于接收者的指定key path的值. key path序列中不符合特定键的键值编码的任何对象(即valueForKey:的默认实现无法找到访问器方法)都接收到valueForUndefinedKey:消息
  • dictionaryWithValuesForKeys:
    • 返回相对于接收者的键数组的值. 该方法为数组中的每个键调用valueForKey:返回的NSDictionary包含数组中所有键的值

注意 集合对象例如NSArray, NSSetNSDictionary不能包含nil作为值. 而是使用NSNull对象表示nil值. NSNull提供表示为对象属性中的空值的单个实例. dictionaryWithValuesForKeys:和相关的setValuesForKeysWithDictionary:的默认实现自动在NSNull(在dictionary参数中)和nil(在存储的属性中)之间转换

当使用key path寻址属性时, 如果除key path中的最后一个键以外的其他键是一对多关系(即它引用一个集合), 则返回的值是一个包含键所有值的集合. 例如,请求key path, transactions.payee的值将返回一个数组, 其中包含所有transaction的所有payee对象. 这也适用于key path中的多个数组. key path account.transactions.payee返回一个数组其中包含所有帐户中所有交易的所有受款人对象

Setting Attribute Values Using Keys

getter一样, 与键值编码兼容的对象还基于NSObject中提供的NSKeyValueCoding协议的实现, 为一小组具有默认行为的通用设置器提供了默认行为

  • setValue:forKey:
    • 将相对于接收消息的对象的指定键的值设置为给定值. setValue:forKey:的默认实现自动解包装表示标量和结构的NSNumberNSValue对象并将它们分配给属性
    • 如果指定的键对应于接收setter调用的对象所不具有的属性, 则该对象将向自身发送setValue:forUndefinedKey:消息
    • setValue:forUndefinedKey:的默认实现抛出NSUndefinedKeyException异常. 但是子类可以重写此方法以自定义方式处理
  • setValue:forKeyPath:
    • 在相对于接收者的指定key path处设置给定值. key path序列中不符合特定键的键值编码的任何对象都会收到setValue:forUndefinedKey:消息
  • setValuesForKeysWithDictionary:
    • 使用字典键标识属性, 使用指定字典中的值设置接收器的属性.
    • 默认实现为每个键值对调用setValue:forKey:并根据需要用nil代替NSNull对象

在默认实现中, 当尝试将非对象属性设置为nil值时,符合键值编码的对象会向自身发送setNilValueForKey:消息. setNilValueForKey:的默认实现抛出NSInvalidArgumentException但是对象可以覆盖此行为以替代默认值或标记值

Using Keys to Simplify Object Access

若要查看基于键的gettersetter如何简化的代码, 请考虑以下示例. 在macOS中,NSTableViewNSOutlineView对象将标识符字符串与其各自的列相关联. 如果支持该表的模型对象不符合键值编码, 则该表的数据源方法必须依次检查每个列标识符, 以找到要返回的正确属性,如下面的示例所示. 此外, 在将来, 向模型中添加另一个属性(在这种情况下为Person对象)时, 还必须重新访问数据源方法, 添加另一个条件以测试新属性并返回相关值

没有使用键值编码的数据源方法的实现

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    id result = nil;
    Person *person = [self.people objectAtIndex:row];
 
    if ([[column identifier] isEqualToString:@"name"]) {
        result = [person name];
    } else if ([[column identifier] isEqualToString:@"age"]) {
        result = @([person age]);  // Wrap age, a scalar, as an NSNumber
    } else if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        result = [person favoriteColor];
    } // And so on...
 
    return result;
}

另一方面,下面的示例代码显示了同一数据源方法的更紧凑的实现, 该方法利用了与键值编码兼容的Person对象. 仅使用valueForKey: getter, 数据源方法使用列标识符作为键返回适当的值. 除了更短之外它也更通用, 因为只要以后的列标识符始终与模型对象的属性名称匹配, 当以后添加新列时它就继续保持不变

键值编码的数据源方法的实现

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}

Accessing Collection Properties

符合键值编码的对象会和其他属性一样公开"一对多"的属性(集合属性). 可以使用valueForKey:setValue:forKey(或其等效的key path)来获取或设置集合对象, 就像其他任何对象一样. 当想操纵这些集合的内容时, 通常最有效的方法是使用协议定义的可变代理方法

该协议定义了三种不同的代理对象访问代理方法,每种方法都有一个keykey path变体

  • mutableArrayValueForKey:, mutableArrayValueForKeyPath:
    • 返回行为类似于NSMutableArray对象的代理对象
  • mutableSetValueForKey: , mutableSetValueForKeyPath:
    • 返回行为类似于NSMutableSet对象的代理对象
  • mutableOrderedSetValueForKey: , mutableOrderedSetValueForKeyPath:
    • 返回行为类似于NSMutableOrderedSet对象的代理对象

当对代理对象进行操作, 向其中添加对象, 从中删除对象或替换其中的对象时, 协议的默认实现都会相应地修改底层属性. 这比使用valueForKey:获取一个非可变的集合对象创建一个内容已更改的修改对象, 然后使用setValue:forKey:消息将其存储回该对象更为有效. 在许多情况下, 它比直接使用可变属性更有效. 这些方法提供了额外的好处即可以对收集对象中持有的对象保持键值观察遵从性

Using Collection Operators

当发送与键值编码兼容的对象valueForKeyPath:消息时, 可以将集合运算符嵌入到key path中.集合运算符是一小部分关键字之一,其后带有一个at符号(@), 该符号指定getter在返回数据之前以某种方式操作执行数据操作. 由NSObject提供的valueForKeyPath的默认实现会实现此操作

key path包含集合运算符时, 该key path前面的任何部分(称为左key path), 指示相对于消息的接收者进行操作的集合. 如果将消息直接发送到集合对象(例如NSArray实例), 则可以省略左key path, 运算符之后的key path部分(称为右key path)指定了运算符应在集合中进行操作的属性. @count以外的所有集合运算符都需要正确的key path

key path操作符格式

Operator key path format

集合运算符表现出三种基本行为类型

  • 聚合运算符
    • 聚合运算符以某种方式合并集合的对象, 并返回通常与右key path中命名的属性的数据类型匹配的单个对象
    • @count运算符是一个例外, 它不使用任何右key path并且始终返回NSNumber实例
  • 数组运算符
    • 数组运算符返回一个NSArray实例, 其中包含命名集合中保存的对象的某些子集
  • 嵌套运算符
    • 嵌套运算符在包含其他集合的集合上工作, 并返回NSArrayNSSet实例(取决于运算符), 该实例以某种方式组合了嵌套集合的对象

Sample Data

下面的描述包括代码片段, 这些代码片段演示了如何调用每个运算符以及这样做的结果

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
 
@end


@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end

为了便于讨论假设BankAccount实例具有一个填充了下表中所示数据的事务数组,并且从BankAccount对象内部进行了示例调用

Transactions对象的示例数据

收款人payee values 金额格式设置为货币amount 日期值格式为月日年date
Green Power $120.00 Dec 1, 2015
Green Power $150.00 Jan 1, 2016
Green Power $170.00 Feb 1, 2016
Car Loan $250.00 Jan 15, 2016
Car Loan $250.00 Feb 15, 2016
Car Loan $250.00 Mar 15, 2016
General Cable $120.00 Dec 1, 2015
General Cable $155.00 Jan 1, 2016
General Cable $120.00 Feb 1, 2016
Mortgage $1,250.00 Jan 15, 2016
Mortgage $1,250.00 Feb 15, 2016
Mortgage $1,250.00 Mar 15, 2016
Animal Hospital $600.00 Jul 15, 2016

Aggregation Operators

聚合运算符可对数组或一组属性进行操作产生一个可反映集合某些方面的值

@avg

当指定@avg运算符时, valueForKeyPath:会为集合的每个元素读取由右key path指定的属性, 将其转换为double(将0替换为nil值), 然后计算这些值的算术平均. 然后, 它返回NSNumber实例

要获取表中样本数据之间的平均交易金额

NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
// transactionAverage = $456.54

@count

当指定@count运算符时valueForKeyPath:返回NSNumber类型实例, 数值为集合中的对象个数. 右key path(如果存在)将被忽略

获取交易中交易对象的数量

NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
// numberOfTransactions = 13

@max

当指定@max运算符时valueForKeyPath:在由右key path命名的集合条目中搜索并返回最大的条目. 该搜索使用compare:方法进行比较, 该方法在许多Foundation类中(例如NSNumber类)定义. 因此右key path指示的属性必须包含一个有意义地响应此消息的对象. 搜索将忽略nil值的集合条目

在表中列出的事务中获取日期值的最大值, 该值是最近一笔事务的日期

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
//latestDate = Jul 15, 2016

@min

当指定@min运算符时valueForKeyPath:在由右key path名的集合条目中搜索并返回最小的一个. 该搜索使用compare:方法进行比较, 该方法在许多Foundation类中(例如NSNumber类)定义. 因此右key path指示的属性必须包含一个有意义地响应此消息的对象. 搜索将忽略nil值的集合条目

要获取表中列出的交易中最早的交易日期

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
// earliestDate = Dec 1, 2015

@sum

当指定@sum运算符时valueForKeyPath:会为集合的每个元素读取由右key path指定的属性, 将其转换为double类型(将nil值替换为0), 然后计算它们的总和. 返回一个NSNumber`类型的实例

获取表中样本数据之间的交易金额之和

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];

Array Operators

数组运算符使valueForKeyPath:返回一个对象数组, 该对象数组与右key path指示的一组特定对象相对应.

重要 使用数组运算符时, 如果任何叶子对象为nilvalueForKeyPath:方法将抛出异常

@distinctUnionOfObjects

当指定@distinctUnionOfObjects运算符时valueForKeyPath:创建并返回一个数组, 该数组包含与右key path指定的属性相对应的集合的不同对象

获取一个去了重的payee数组

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

生成的distinctPayees数组只包含下列字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage.(筹款人每个人只留一条记录)

注意 @unionOfObjects运算符提供类似的行为但不删除重复的对象

@unionOfObjects

当指定@unionOfObjects运算符时valueForKeyPath:创建并返回一个数组, 该数组包含与右key path指定的属性相对应的集合的所有对象, 不同于@distinctUnionOfObjects 重复的记录不会被删除

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

返回的payees数组包含字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital.

Nesting Operators

嵌套运算符对嵌套集合进行操作, 其中集合的每个条目本身都包含一个集合.

重要 使用嵌套运算符时如果任何叶子对象为nilvalueForKeyPath:方法将抛出异常.

再添加一个样例moreTransactions数组

NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];

moreTransactions数组的示例数据

收款人payee values 金额格式设置为货币amount 日期值格式为月日年date
General Cable - Cottage $120.00 Dec 18, 2015
General Cable - Cottage $155.00 Jan 9, 2016
General Cable - Cottage $120.00 Dec 1, 2016
Second Mortgage $1,250.00 Nov 15, 2016
Second Mortgage $1,250.00 Sep 20, 2016
Second Mortgage $1,250.00 Feb 12, 2016
Hobby Shop $600.00 Jun 14, 2016

@distinctUnionOfArrays

当指定@distinctUnionOfArrays运算符时valueForKeyPath:创建并返回一个数组, 该数组包含与右key path指定的属性对应的所有集合的组合的不同对象

NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];

返回的collectedDistinctPayees包含的数据为:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power(两个数组去重的结果)

@unionOfArrays

当指定@unionOfArrays运算符时valueForKeyPath:创建并返回一个数组, 该数组包含与右key path指定的属性相对应的所有集合的组合的所有对象, 而且不会删除重复项

NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

返回的collectedPayees的结果为:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop

@distinctUnionOfSets

当指定@distinctUnionOfSets运算符时valueForKeyPath:创建并返回一个NSSet对象, 该对象包含与右key path指定的属性相对应的所有集合组合的不同对象

该运算符的行为与@distinctUnionOfArrays相似, 只是它返回的是包含对象的NSSet实例而不是NSArray实例. 假设示例数据已存储在sets中而不是sets中, 则示例调用和结果与@distinctUnionOfArrays所示的相同

Representing Non-Object Values

NSObject提供的键-值编码协议方法的默认实现可同时用于对象和非对象属性. 默认实现自动在对象参数、返回值与非对象属性之间转换. 即使存储的属性是标量或结构体, 这也允许基于键的gettersetter的签名保持一致

重要 由于Swift中的所有属性都是对象, 因此本节仅介绍Objective-C属性

当调用协议的一种getters例如valueForKey:时, 默认实现将根据Accessor Search Patterns描述的规则来确定为指定键提供值的特定访问器方法或实例变量. 如果返回值不是对象, 则getter将使用此值初始化NSNumber对象(用于标量)或NSValue对象(用于结构体)并返回该值

类似地默认情况下, 使用setValue:forKey:之类的setter, 在给定特定key的情况下, 确定属性的访问器或实例变量所需的数据类型. 如果数据类型不是对象, 则setter首先将适当的<type>Value消息发送到传入值对象以提取底层数据然后存储该数据

注意 当为非对象属性调键值编码协议setters之一方法, 传值为nil的时, 该setters不会有反应. 它将setNilValueForKey:消息发送给接收setter调用的对象. 此方法的默认实现抛出NSInvalidArgumentException异常,但是子类可以覆盖此行为如Handling Non-Object Values中所述:例如设置标记值或提供有意义的默认值

Wrapping and Unwrapping Scalar Types

下表列出默认键值编码实现使用NSNumber实例包装的标量类型. 对于每种数据类型, 该表显示了用于从底层属性值初始化NSNumber以提供getter返回值的创建方法(creation method -- setter). 然后显示访问器方法(accessor method -- getter), 该方法用于在设置操作期间从setter输入参数中提取值

包装在NSNumber对象中的标量类型

Data type(类型) Creation method(setter) Accessor method(getter)
BOOL numberWithBool: boolValue (in iOS) charValue (in macOS)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

注意macOS中由于历史原因, BOOL的类型定义为带符号的charKVC不区分这两个字符. 因此当键是BOOL时, 不应将诸如@:"true"@"YES"之类的字符串值传递给setValue:forKey:, KVC会尝试调用charValue(因为BOOL本质上是一个char), 但是NSString没有实现此方法, 从而导致运行时错误. 相反当键是BOOL时, 应仅传递一个NSNumber对象, 例如@(1)@(YES)作为setValue:forKey:value参数. 此限制在iOS中不适用, iOSBOOL的类型定义为本地布尔类型boolKVC调用boolValue, 该布尔值适用于NSNumber对象或格式正确的NSString对象

Wrapping and Unwrapping Structures

下表显示默认访问器用于装包和解包NSPoint, NSRange, NSRectNSSize结构体的creationaccessor方法

使用1NSValue1包装的常见结构类型

Data type(类型) Creation method(setter) Accessor method(getter)
NSPoint valueWithPoint: pointValue
NSRange valueWithRange: rangeValue
NSRect valueWithRect: (macOS only). rectValue
NSSize valueWithSize: sizeValue

自动装包和解包不限于NSPoint, NSRange, NSRectNSSize. 可以将结构体类型(即Objective-C类型编码字符串以{开头的类型)包装在NSValue对象中

使用自定义结构的示例类

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

使用该类的名为myClass的实例可以通过键值编码获得threeFloats

NSValue* result = [myClass valueForKey:@"threeFloats"];

valueForKey:的默认实现会调用threeFloatsgetter, 然后返回包装在NSValue对象中的结果

同样可以使用键值编码设置threeFloats

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

默认实现会调用getValue:方法进行解包, 然后将解包的结果使用setThreeFloats:方法传入进行设置

Validating Properties

键值编码协议定义支持属性验证的方法. 就像使用基于键的accessors读取和写入符合键值编码的对象的属性一样, 也可以按key(或key path)验证属性. 当调用validateValue:forKey:error:(或者validateValue:forKeyPath:error:)方法, 协议的默认实现在接收验证消息的对象(或key path末尾的对象)中搜索一种名称与模式validate<Key>:error:相匹配的方法. 如果对象没有这种方法, 则默认情况下验证成功, 并且默认实现返回YES. 当存在特定于属性的验证方法时, 默认实现将返回调用该方法的结果.

注意 通常仅在Objective-C中使用此处描述的验证. 在Swift中, 依赖于编译器对可选项和强类型检查的支持, 同时使用内置的willSetdidSet属性观察器来测试任何运行时API接口, 如The Swift Programming Language (Swift 3)The Property Observers部分中所述

由于特定于属性的验证方法通过引用接收值和错误参数, 因此验证具有三种可能的结果

  • 验证方法认为值对象有效, 并且在不更改值或错误的情况下返回YES
  • 验证方法认为值对象无效, 但选择不对其进行更改. 在这种情况下该方法返回NO并将错误引用(如果由调用者提供)设置为指示错误原因的NSError对象
  • 验证方法认为值对象无效, 但是创建了一个新的有效对象作为替换. 在这种情况下, 该方法返回YES同时保持错误对象不变. 在返回之前, 该方法将值引用修改为指向新值对象. 进行修改时即使值对象是可变的, 该方法也始终会创建一个新对象而不是修改旧对象

验证name名称属性

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}

Automatic Validation

通常, 键值编码协议及其默认实现均未定义任何机制来自动执行验证. 而是在适合应用时使用验证方法. 某些其他Cocoa技术在某些情况下会自动执行验证. 例如保存托管对象上下文时, Core Data会自动执行验证. 在macOS中, Cocoa绑定允许指定验证应自动进行

Accessor Search Patterns

NSObject提供的NSKeyValueCoding协议的默认实现使用一组明确定义的规则将基于keyaccessor调用映射到对象的底层属性. 这些协议方法使用关key参数在自己的对象实例中搜索遵循某些命名约定的访问器, 实例变量和相关方法. 尽管很少修改此默认搜索, 但它对于跟踪键值编码对象的行为以及使自己的对象符合标准既有帮助,也有助于理解它的工作方式.

注意 本节中的描述使用<key>或<key>字符串的占位符, 该key字符串在一种键值编码协议方法中作为参数出现, 然后由该方法用作辅助方法调用的一部分或用于变量名查找. 属性映射名称复合站位符规则. 例如getters<key>is<Key>, 属性名hidden将映射为hiddenisHidden

Search Pattern for the Basic Getter

在给定key参数作为输入的情况下valueForKey:的默认实现执行以下过程, 从接收valueForKey:的类实例内部执行操作

  1. 在实例中搜索找到的第一个访问器(accessor)方法, 其名称类似于get<Key>, <key>, is<Key>, _<key>, 以该顺序, 如果找到, 则调用它并执行步骤5否则继续下一步
  2. 如果找不到简单的访问器(accessor)方法, 请在实例中搜索名称与模式匹配的方法countOf<Key>, objectIn<Key>AtIndex:(对应于NSArray类定义的方法), <key>AtIndexes:(对应于NSArray方法objectsAtIndexes:)
    • 如果找到其中的第一个以及其他两个中的至少一个, 请创建一个响应所有NSArray方法的集合代理对象, 并返回该对象. 否则请继续执行步骤3
    • 代理对象随后将接收到的所有NSArray消息转换为以下countOf<Key>, objectIn<Key>AtIndex:, <key>AtIndexes:消息的某种组合, 以创建键值编码对象. 如果原始对象还实现了一个名称如下的可选方法get<Key>:range:, 代理对象也会在适当的时候使用它. 实际上代理对象与与键值编码兼容的对象一起使用, 可以使底层属性的行为就好像它是NSArray一样, 即使不是.
  3. 如果找不到简单的访问器方法或数组访问方法组, 请查找名为countOf<Key>, enumeratorOf<Key>, memberOf<Key>:(对应于NSSet类定义的方法)
    • 如果三个方法找到了, 请创建一个响应所有NSSet方法的集合代理对象, 并返回该对象. 否则请继续执行步骤4
    • 此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>, enumeratorOf<Key>, memberOf<Key>:消息的某种组合, 以创建它的对象. 实际上代理对象与与键值编码兼容的对象一起使用, 可以使底层属性的行为就好像它是NSSet一样, 即使不是.
  4. 如果找不到简单的访问器方法或集合访问方法, 并且接收者的类方法accessInstanceVariablesDirectly返回YES则以_<key>, _is<Key>, <key>, is<Key>,顺序搜索示例变量名. 如果找到请直接获取实例变量的值, 然后继续执行步骤5. 否则请继续执行步骤6
  5. 如果检索到的属性值是对象指针则只需返回结果, 如果该值是NSNumber支持的标量类型则将其存储在NSNumber实例中并返回它. 如果结果是NSNumber不支持的标量类型请转换为NSValue对象并返回该对象
  6. 如果所有其他方法均失败则调用valueForUndefinedKey:. 默认情况下这会抛出一个异常, 但是NSObject的子类可以提供特定于键的行为(重写valueForUndefinedKey:, 自定义相应)

gnustep-base-1.25.0版本的valueForKey:实现(NSKeyValueCoding.m line:520)

GNUstep Base1.25.0已是旧版, 点击下载查看新版

- (id) valueForKey: (NSString*)aKey
{
  unsigned	size = [aKey length] * 8;
  char		key[size + 1];

  [aKey getCString: key
	 maxLength: size + 1
	  encoding: NSUTF8StringEncoding];
  size = strlen(key);
  return ValueForKey(self, key, size);
}

static id ValueForKey(NSObject *self, const char *key, unsigned size)
{
  SEL		sel = 0;
  int		off = 0;
  const char	*type = NULL;

  if (size > 0)
    {
      const char	*name;
      char		buf[size + 5];
      char		lo;
      char		hi;

      strncpy(buf, "_get", 4);
      strncpy(&buf[4], key, size);
      buf[size + 4] = '\0';
      lo = buf[4];
      hi = islower(lo) ? toupper(lo) : lo;
      buf[4] = hi;

      name = &buf[1];	// getKey
      sel = sel_getUid(name);
      if (sel == 0 || [self respondsToSelector: sel] == NO)
	{
	  buf[4] = lo;
	  name = &buf[4];	// key
	  sel = sel_getUid(name);
	  if (sel == 0 || [self respondsToSelector: sel] == NO)
	    {
              buf[4] = hi;
              buf[3] = 's';
              buf[2] = 'i';
              name = &buf[2];	// isKey
              sel = sel_getUid(name);
              if (sel == 0 || [self respondsToSelector: sel] == NO)
                {
                  sel = 0;
                }
	    }
	}

      if (sel == 0 && [[self class] accessInstanceVariablesDirectly] == YES)
	{
	  buf[4] = hi;
	  name = buf;	// _getKey
	  sel = sel_getUid(name);
	  if (sel == 0 || [self respondsToSelector: sel] == NO)
	    {
	      buf[4] = lo;
	      buf[3] = '_';
	      name = &buf[3];	// _key
	      sel = sel_getUid(name);
	      if (sel == 0 || [self respondsToSelector: sel] == NO)
		{
		  sel = 0;
		}
	    }
	  if (sel == 0)
	    {
	      if (GSObjCFindVariable(self, name, &type, &size, &off) == NO)
		{
                  buf[4] = hi;
                  buf[3] = 's';
                  buf[2] = 'i';
                  buf[1] = '_';
                  name = &buf[1];	// _isKey
		  if (!GSObjCFindVariable(self, name, &type, &size, &off))
                    {
                       buf[4] = lo;
                       name = &buf[4];		// key
		       if (!GSObjCFindVariable(self, name, &type, &size, &off))
                         {
                            buf[4] = hi;
                            buf[3] = 's';
                            buf[2] = 'i';
                            name = &buf[2];	// isKey
                            GSObjCFindVariable(self, name, &type, &size, &off);
                         }
                    }
		}
	    }
	}
    }
  return GSObjCGetVal(self, key, sel, type, size, off);
}

Search Pattern for the Basic Setter

setValue:forKey:的默认实现将给定键和值参数作为输入, 尝试将名为key的属性设置为value, 实现内部有如下步骤

  1. 按名为的set<Key>: , _set<Key>顺序查找第一个accessor访问器, 如果找到则使用输入值(或根据需要解包的值)调用它并完成
  2. 如果找不到简单的访问器, 并且类方法accessInstanceVariablesDirectly返回YES则按该顺序查找名称形如_<key>, _is<Key>, <key>, is<Key>的实例变量. 如果找到则直接使用输入值(解包)设置变量并完成操作
  3. 在找不到访问器或实例变量后, 调用setValue:forUndefinedKey:, 默认情况下这会抛出一个异常, 但是NSObject的子类可以提供特定于键的行为(重写setValue:forUndefinedKey:, 自定义相应)

gnustep-base-1.25.0版本的valueForKey:实现(NSKeyValueCoding.m line:351)

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  unsigned	size = [aKey length] * 8;
  char		key[size + 1];
#ifdef WANT_DEPRECATED_KVC_COMPAT
  IMP   	o = [self methodForSelector: @selector(takeValue:forKey:)];

  setupCompat();
  if (o != takeValue && o != takeValueKVO)
    {
      (*o)(self, @selector(takeValue:forKey:), anObject, aKey);
      return;
    }
#endif

  [aKey getCString: key
	 maxLength: size + 1
	  encoding: NSUTF8StringEncoding];
  size = strlen(key);
  SetValueForKey(self, anObject, key, size);
}

static void
SetValueForKey(NSObject *self, id anObject, const char *key, unsigned size)
{
  SEL		sel = 0;
  const char	*type = 0;
  int		off = 0;

  if (size > 0)
    {
      const char	*name;
      char		buf[size + 6];
      char		lo;
      char		hi;

      strncpy(buf, "_set", 4);
      strncpy(&buf[4], key, size);
      lo = buf[4];
      hi = islower(lo) ? toupper(lo) : lo;
      buf[4] = hi;
      buf[size + 4] = ':';
      buf[size + 5] = '\0';

      name = &buf[1];	// setKey:
      type = NULL;
      sel = sel_getUid(name);
      if (sel == 0 || [self respondsToSelector: sel] == NO)
	{
	  name = buf;	// _setKey:
	  sel = sel_getUid(name);
	  if (sel == 0 || [self respondsToSelector: sel] == NO)
	    {
	      sel = 0;
	      if ([[self class] accessInstanceVariablesDirectly] == YES)
		{
		  buf[size + 4] = '\0';
		  buf[3] = '_';
		  buf[4] = lo;
		  name = &buf[3];	// _key
		  if (GSObjCFindVariable(self, name, &type, &size, &off) == NO)
		    {
		      buf[4] = hi;
		      buf[3] = 's';
		      buf[2] = 'i';
		      buf[1] = '_';
		      name = &buf[1];	// _isKey
		      if (GSObjCFindVariable(self,
			name, &type, &size, &off) == NO)
			{
			  buf[4] = lo;
			  name = &buf[4];	// key
			  if (GSObjCFindVariable(self,
			    name, &type, &size, &off) == NO)
			    {
			      buf[4] = hi;
			      buf[3] = 's';
			      buf[2] = 'i';
			      name = &buf[2];	// isKey
			      GSObjCFindVariable(self,
				name, &type, &size, &off);
			    }
			}
		    }
		}
	    }
	  else
	    {
	      GSOnceFLog(@"Key-value access using _setKey: is deprecated:");
	    }
	}
    }
  GSObjCSetVal(self, key, anObject, sel, type, size, off);
}

Search Pattern for Mutable Arrays

mutableArrayValueForKey:的默认实现, 给定键参数作为输入, 使用以下过程为对象内部接收访问器调用的名为key的属性返回可变代理数组

  1. 寻找一对名称如insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex:的方法(分别对应于NSMutableArray的方法insertObject:atIndex:removeObjectAtIndex:), 或者形如insert<Key>:atIndexes:, remove<Key>AtIndexes:的方法(相对于 NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:)
    • 如果对象具有至少一种插入方法和至少一种移除方法, 则通过发送insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:, remove<Key>AtIndexes:的某种组合来返回响应NSMutableArray消息的代理对象.
    • 当接收到mutableArrayValueForKey:消息的对象还实现了一个可选的replace对象方法,其名称类似于replaceObjectIn<Key>AtIndex:withObject: , replace<Key>AtIndexes:with<Key>:时, 代理对象也会在适当时使用这些方法
  2. 如果对象没有可变数组方法, 则查找名称与模式set<Key>:匹配的访问器方法. 在这种情况下, 通过向mutableArrayValueForKey:的原始接收者发出set<Key>:消息, 返回响应NSMutableArray消息的代理对象

注意 此步骤中描述的机制比上一步的机制效率低得多, 因为它可能涉及重复创建新的集合对象而不是修改现有集合对象. 因此在设计自己的键值编码兼容对象时, 通常应避免使用它

  1. 如果既没有找到可变数组方法, 也没有找到访问器, 并且接收方的类对accessInstanceVariablesDirectly响应为YES, 则按_<key> , <key>顺序搜索实例变量
    • 如果找到了这样的实例变量则返回一个代理对象, 该代理对象将接收到的每个NSMutableArray消息转发到该实例变量的值, 该值通常是NSMutableArray的实例或其子类之一
  2. 如果所有其他方法均失败, 则每当接收到NSMutableArray消息时, 就向mutableArrayValueForKey:消息的原始接收者返回一个引起setValue:forUndefinedKey:消息的可变集合代理对象
    • setValue:forUndefinedKey:会抛出NSUndefinedKeyException异常, 可重写进行处理

gnustep-base-1.25.0版本的mutableArrayValueForKey:实现(NSKeyValueMutableArray.m line:305)

- (NSMutableArray*) mutableArrayValueForKey: (NSString*)aKey
{
  return [NSKeyValueMutableArray arrayForKey: aKey ofObject: self];
}

+ (NSKeyValueMutableArray *) arrayForKey: (NSString *)aKey
                                ofObject: (id)anObject
{
  NSKeyValueMutableArray *proxy;
  unsigned size = [aKey maximumLengthOfBytesUsingEncoding: 
			  NSUTF8StringEncoding];
  char keybuf[size + 1];
  [aKey getCString: keybuf
         maxLength: size + 1
          encoding: NSUTF8StringEncoding];
  if (islower(*keybuf))
    {
      *keybuf = toupper(*keybuf);
    }

  proxy = [NSKeyValueFastMutableArray arrayForKey: aKey 
				         ofObject: anObject
			       withCapitalizedKey: keybuf];
  if (proxy == nil)
    {
      proxy = [NSKeyValueSlowMutableArray arrayForKey: aKey 
  					     ofObject: anObject
				   withCapitalizedKey: keybuf];

      if (proxy == nil)
	{
	  proxy = [NSKeyValueIvarMutableArray arrayForKey: aKey 
					         ofObject: anObject];
	}
    }
  return proxy;
}

Search Pattern for Mutable Ordered Sets

mutableOrderedSetValueForKey:的默认实现与valueForKey:识别相同的简单访问器方法和有序集合访问器方法, 并遵循相同的直接实例变量访问策略, 但始终返回一个可变的集合代理对象, valueForKey:返回的不可变集合. 此外它还执行以下操作

  1. 搜索形如insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex:(对应于NSMutableOrderedSet类定义的两个的方法)的方法, 以及insert<Key>:atIndexes: , remove<Key>AtIndexes:方法(对应于insertObjects:atIndexes:removeObjectsAtIndexes:)

    • 如果找到至少一种插入方法和至少一种删除方法, 则返回的代理对象将发送insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:, remove<Key>AtIndexes:发送到mutableOrderedSetValueForKey:消息的原始接收者, 当它收到NSMutableOrderedSet消息时
    • 代理对象还使用诸如replaceObjectIn<Key>AtIndex:withObject: , replace<Key>AtIndexes:with<Key>:之类名称的方法, 如果原始对象有这些方法.
  2. 如果找不到可变的集合方法, 请搜索名称为set<Key>:的访问器方法. 在这种情况下, 返回的代理对象每次接收到NSMutableOrderedSet消息时, 都会向mutableOrderedSetValueForKey:的原始接收者发送set<Key>:消息

注意 此步骤中描述的机制比上一步的机制效率低得多, 因为它可能涉及重复创建新的集合对象而不是修改现有集合对象. 因此在设计自己的键值编码兼容对象时, 通常应避免使用它

  1. 如果既找不到可变集合消息也没有找到访问器, 并且接收者的accessInstanceVariablesDirectly类方法返回YES, 则按名称形如_<key> , <key>的顺序搜索实例变量. 如果找到了这样的实例变量, 则返回的代理对象会将收到的所有NSMutableOrderedSet消息转发到该实例变量的值, 该值通常是NSMutableOrderedSet的实例或其子类之一
  2. 如果其他所有方法均失败, 则返回的代理对象每收到可变的集合消息, 就会将setValue:forUndefinedKey:消息发送给mutableOrderedSetValueForKey:的原始接收者
    • setValue:forUndefinedKey:会抛出NSUndefinedKeyException异常, 可重写进行处理

Search Pattern for Mutable Sets

mutableSetValueForKey:的默认实现, 给定键参数作为输入, 为接收访问器调用的对象内的名为key的数组属性返回一个可变代理, 使用以下步骤

  1. 搜索形如add<Key>Object: , remove<Key>Object:的方法(分别对应于NSMutableSet方法addObject:removeObject:), 还有add<Key>: , remove<Key>:方法(对应于NSMutableSet方法unionSet:minusSet:). 如果找到至少一种添加方法和至少一种移除方法, 则返回一个代理对象, 该对象发送add<Key>Object:, remove<Key>Object:, add<Key>:, remove<Key>:的某种组合, 消息发送给mutableSetValueForKey:的原始接收者, 对于它收到的每个NSMutableSet消息
    • 代理对象也会使用名如intersect<Key>: , set<Key>:的方法, 如果这些方法存在的话
  2. 如果mutableSetValueForKey:调用的接收者是托管对象, 则搜索模式不会像非托管对象那样继续. 详情查看Core Data Programming Guide
  3. 如果找不到可变集合的方法, 并且该对象不是托管对象, 则搜索名称类似于set<Key>:的访问器方法. 如果找到了这样的方法, 则返回的代理对象针对接收到的每个NSMutableSet消息, 向mutableSetValueForKey:的原始接收者发送set<Key>:消息

注意 此步骤中描述的机制比上一步的机制效率低得多, 因为它可能涉及重复创建新的集合对象而不是修改现有集合对象. 因此在设计自己的键值编码兼容对象时, 通常应避免使用它

  1. 如果未找到可变的集合方法和访问器方法, 并且accessInstanceVariablesDirectly类方法返回YES, 则按该顺序搜索名称如_<key> , <key>的实例变量。如果找到了这样的实例变量,则代理对象会将收到的每个NSMutableSet消息转发给该实例变量的值, 该值通常是NSMutableSet的实例或其子类之一
  2. 如果所有其他方法均失败, 则返回的代理对象通过将setValue:forUndefinedKey:消息发送到mutableSetValueForKey:的原始接收者来响应它收到的任何NSMutableSet消息

Adopting Key-Value Coding

Achieving Basic Key-Value Coding Compliance

对对象采用键值编码时, 通过让对象继承自NSObject或其许多子类之一, 可以依靠NSKeyValueCoding协议的默认实现. 相应地默认实现依赖于按照某些定义明确的模式来定义对象的实例变量(或ivars)和访问器方法, 以便在接收键值编码的消息(例如valueForKey:setValue:forKey:)可以将键字符串与属性关联

通常通过简单地使用@property语句声明属性, 并允许编译器自动合成ivar和访问器, 来遵守Objective-C中的标准模式, 编译器默认遵循预期模式

注意Swift中只需以通常的方式声明属性即可自动生成适当的访问器, 而永远不会直接与ivars进行交互. 有关Swift中的属性的更多信息, 请阅读The Swift Programming Language(Swift 3)中的Properties. 有关与Swift中的Objective-C属性进行交互的特定信息, 请阅读Using Swift with Cocoa and Objective-C (Swift 3)中的Accessing Properties

如果确实需要在Objective-C中手动实现访问器或ivars, 请遵循本节中的指南以保持基本的合规性. 要提供其他功能来增强与任何语言的对象的集合属性的交互, 请实现Defining Collection Methods.中所述的方法. 要通过键值验证进一步增强对象, 请实现Adding Validation中所述的方法

注意 键值编码的默认实现可使用比此处描述的更广泛的ivars和访问器. 如果有使用其他变量或访问器约定的旧代码, 请检查访问器搜索模式中的搜索模式, 以确定默认实现是否可以找到对象的属性.

Basic Getters

要实现返回属性值的getter, 同时可能还要执行其他自定义工作, 请使用名字和property相同的方法. 例如title字符串属性

- (NSString*)title
{
   // Extra getter logic…
 
   return _title;
}

对于具有布尔值的属性也可以使用以is前缀的方法例如对于hidden的布尔属性

- (BOOL)isHidden
{
   // Extra getter logic…
 
   return _hidden;
}

当属性是标量或结构体时, 键值编码的默认实现将值包装在对象中, 以便在协议方法的接口上使用. 无需执行任何特殊操作即可支持此行为

Basic Setters

要实现用于存储属性值的设置器, 请使用带有属性大写名称并以单词set开头的方法, 比如hidden属性的set

- (void)setHidden:(BOOL)hidden
{
    // Extra setter logic…
 
   _hidden = hidden;
}

警告 切勿从set<Key>:方法内部调用验证属性中描述的验证方法

当属性是非对象类型时, 例如布尔类型的hidden, 协议的默认实现会检测底层数据类型, 并在将其应用于setter之前解包来自setValue:forKey:的对象值(在这种情况下为NSNumber实例). 无需在设置器中处理此问题. 但是如果有可能将nil值写入非对象属性. 则可以重写setNilValueForKey:来处理这种情况

hidden属性可能将nil合理的解释为NO

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"hidden"]) {
        [self setValue:@(NO) forKey:@”hidden”];
    } else {
        [super setNilValueForKey:key];
    }
}

即使允许编译器合成setter, 也可以在适当时提供上述方法重写

Instance Variables

当键值编码访问器方法之一的默认实现找不到属性的访问器时, 它将查询其类的accessInstanceVariablesDirectly方法, 以查看该类是否允许直接使用实例变量. 默认情况下该类方法返回YES, 也可以重写此方法以返回NO

如果确实允许使用ivars请确保以常规方式命名, 并使用带下划线(_)前缀的属性名称. 通常编译器会在自动合成属性时执行此操作, 但是如果使用显式的@synthesize指令则可以自己声明实例变量名

@synthesize title = _title;

在某些情况下, 不使用@synthesize指令或允许编译器自动合成属性, 可以使用@dynamic指令来通知编译器将在运行时提供gettersetter. 可以这样做以避免自动合成一个getter从而可以提供集合访问器. 在这种情况下将ivar`声明为接口声明的一部分

@interface MyObject : NSObject {
    NSString* _title;
}
 
@property (nonatomic) NSString* title;
 
@end

Defining Collection Methods

使用标准命名约定创建访问器和ivars时, 键值编码协议的默认实现可以根据键值编码消息找到它们. 对于表示一对多关系的集合对象和对其他属性而言都是如此. 如果代替集合属性的基本访问器或在集合属性的基础上实现集合访问器方法则可以:

  • 使用NSArrayNSSet以外的类对多个关系进行建模
    • 当在对象中实现集合方法时, 键-值获取器的默认实现会返回一个代理对象, 该代理对象将响应收到的后续NSArrayNSSet消息, 然后调用这些方法. 底层属性对象不必是NSArrayNSSet本身因为代理对象使用的集合方法提供了预期的行为
  • 更改一对多关系的内容时可以提高性能
    • 协议的默认实现不是使用基本的setter重复创建新的集合对象来响应每次更改, 而是使用集合方法来对底层属性进行适当的突变
  • 提供对键值观察的合规访问权限以访问对象的集合属性的内容

实现两种中任一集合访问器, 取决于是否希望关系像集合的索引, 集合是否有序, 或是特定的集合. 无论哪种情况都要实现至少一组方法来支持对该属性的读取访问, 然后添加另一组以启用对集合内容的更改

注意 键值编码协议未声明本节中描述的方法. 而是NSObject提供的协议的默认实现在键值编码兼容对象中查找这些方法. 使用它们来处理作为协议一部分的键值编码的消息

Accessing Indexed Collections

可以添加索引访问器方法以提供一种用于对有序关系中的对象进行计数、检索、添加和替换的机制. 底层对象通常是NSArrayNSMutableArray的实例, 但是如果提供集合访问器则可以将实现了这些方法的任何对象属性当作一个数组来进行操作

Indexed Collection Getters

对于没有默认getter的集合属性, 如果提供以下索引集合getter方法, 则该协议的默认实现会响应valueForKey:消息, 返回行为类似于NSArray的代理对象, 但会调用以下集合工作方法

注意modern版本的Objective-C中编译器默认情况下会为每个属性合成一个getter因此默认实现不会创建使用本节中方法的只读代理. 可以通过不声明属性(仅依靠ivar)或将属性声明为@dynamic来解决此问题, 这表明计划在运行时提供访问器行为. 无论哪种方式编译器都不会提供默认的getter并且默认的实现使用以下方法

countOf<Key>

该方法以NSUInteger的形式返回一对多关系中的对象数, 就像NSArray基本方法count一样. 实际上当底层属性是NSArray时可以使用该方法来提供结果

- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}

objectIn<Key>AtIndex: or <key>AtIndexes:

第一个返回对象在一对多关系中指定索引处的对象,而第二个返回在NSIndexSet参数指定的索引的对象数组. 这些对应于NSArray方法objectAtIndex:objectsAtIndexes:. 只需要实现其中之一即可

- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}
 
- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}

get<Key>:range:

此方法是可选的但可以提高性能. 它从集合中返回属于指定范围的对象. 对应宇于NSArraygetObjects:range:方法

- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
               range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}

Indexed Collection Mutators

要支持与索引访问器之间可变的一对多关系需要实现另一组方法. 当提供这些setter方法时默认的实现会根据mutableArrayValueForKey:消息返回一个代理对象, 该代理对象的行为类似于NSMutableArray对象, 使用该对象的方法来完成其工作. 通常这比直接返回NSMutableArray对象更有效. 它还使一对多关系的内容符合键值观察

为了使对象键值编码符合可变的有序一对多关系请实现以下方法

insertObject:in<Key>AtIndex:or insert<Key>:atIndexes:

第一个接收要插入的对象和一个NSUInteger它指定应将其插入的索引. 第二个对象数组到指定的索引数组处. 这些类似于NSMutableArray方法insertObject:atIndex:insertObjects:atIndexes:. 这些方法只需要一种即可

- (void)insertObject:(Transaction *)transaction
  inTransactionsAtIndex:(NSUInteger)index {
    [self.transactions insertObject:transaction atIndex:index];
}
 
- (void)insertTransactions:(NSArray *)transactionArray
              atIndexes:(NSIndexSet *)indexes {
    [self.transactions insertObjects:transactionArray atIndexes:indexes];
}

removeObjectFrom<Key>AtIndex: or remove<Key>AtIndexes:

第一个接收一个NSUInteger值, 该值指定要从关系中删除的对象的索引. 第二个接收一个NSIndexSet对象, 该对象指定要从关系中删除的对象的索引. 这些方法分别对应于NSMutableArray方法removeObjectAtIndex:removeObjectsAtIndexes:. 这些方法只需要一种即可

- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index {
    [self.transactions removeObjectAtIndex:index];
}
 
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self.transactions removeObjectsAtIndexes:indexes];
}

replaceObjectIn<Key>AtIndex:withObject: or replace<Key>AtIndexes:with<Key>:

方法说明略. 等同于NSMutableArrayreplaceObjectAtIndex:withObject:replaceObjectsAtIndexes:withObjects:方法. 在对应用程序进行性能分析时发现性能问题时,可以选择提供这些方法

- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index
                             withObject:(id)anObject {
    [self.transactions replaceObjectAtIndex:index
                              withObject:anObject];
}
 
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
                    withTransactions:(NSArray *)transactionArray {
    [self.transactions replaceObjectsAtIndexes:indexes
                                withObjects:transactionArray];
}

Accessing Unordered Collections

添加了无序集合访问器方法, 以提供一种用于访问和变异无序关系中的对象的机制. 通常此关系是NSSetNSMutableSet对象的实例. 但是在实现这些访问器时, 可以使任何类对关系进行建模, 并使用键值编码对其进行操作, 它就像是NSSet的实例一样

Unordered Collection Getters

当提供以下集合getter方法以返回集合中的对象数时遍历集合对象, 并测试集合中是否已存在对象(协议的默认实现), 以响应valueForKey:消息. 返回行为类似于NSSet的代理对象, 调用以下收集方法来完成其工作

countOf<Key>

对应于NSSetcount方法

- (NSUInteger)countOfEmployees {
    return [self.employees count];
}

enumeratorOf<Key>

- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}

memberOf<Key>:

- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}

Unordered Collection Mutators

要支持与无序访问者的可变的一对多关系需要实现其他方法. 实现可变的无序访问器, 以允许对象响应mutableSetValueForKey:方法提供无序的集合代理对象. 实现这些访问器比依赖访问器直接返回可变对象以对关系中的数据进行更改要高效得多. 它还使类键值观察符合集合对象

需要实现以下方法

add<Key>Object: or add<Key>:

- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}
 
- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}

remove<Key>Object: or remove<Key>:

- (void)removeEmployeesObject:(Employee *)anObject {
    [self.employees removeObject:anObject];
}
 
- (void)removeEmployees:(NSSet *)manyObjects {
    [self.employees minusSet:manyObjects];
}

intersect<Key>:

- (void)intersectEmployees:(NSSet *)otherObjects {
    return [self.employees intersectSet:otherObjects];
}

Handling Non-Object Values

通常与键值编码兼容的对象依靠键值编码的默认实现来自动装包和解包非对象属性. 可以覆盖默认行为. 这样做的最常见原因是要处理在非对象属性上存储nil值的情况

注意 由于Swift中的所有属性都是对象因此本节仅适用于Objective-C属性

实现如下方法即可

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}

注意

为了向后兼容,当对象已覆盖已弃用的unableToSetNilForKey:方法时,setValue:forKey:会调用该方法而不是setNilValueForKey:

Adding Validation

键值编码协议定义了用于通过键或键路径验证属性的方法. 这些方法的默认实现又依赖于按照类似于用于访问器方法的命名方式来定义方法. 具体来说可以为要验证的具有名为key的任何属性提供一个validate<Key>:error:方法. 默认实现会对此进行搜索以响应键编码的validateValue:forKey:error:消息

如果不提供属性的验证方法则协议的默认实现将假定该属性的验证成功无论其值如何.

gnustep-base-1.25.0版本的validateValue:forKey:error:实现(NSKeyValueCoding.m line:461)

- (BOOL) validateValue: (id*)aValue
                forKey: (NSString*)aKey
                 error: (NSError**)anError
{
  unsigned	size;

  if (aValue == 0 || (size = [aKey length] * 8) == 0)
    {
      [NSException raise: NSInvalidArgumentException format: @"nil argument"];
    }
  else
    {
      char		name[size + 16];
      SEL		sel;
      BOOL		(*imp)(id,SEL,id*,id*);

      strncpy(name, "validate", 8);
      [aKey getCString: &name[8]
	     maxLength: size + 1
	      encoding: NSUTF8StringEncoding];
      size = strlen(&name[8]);
      strncpy(&name[size + 8], ":error:", 7);
      name[size + 15] = '\0';
      if (islower(name[8]))
	{
	  name[8] = toupper(name[8]);
	}
      sel = sel_getUid(name);
      if (sel != 0 && [self respondsToSelector: sel] == YES)
	{
	  imp = (BOOL (*)(id,SEL,id*,id*))[self methodForSelector: sel];
	  return (*imp)(self, sel, aValue, anError);
	}
    }
  return YES;
}

Implementing a Validation Method

name属性的验证方法

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}

Validation of Scalar Values

标量属性的验证方法

- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    if (*ioValue == nil) {
        // Value is nil: Might also handle in setNilValueForKey
        *ioValue = @(0);
    } else if ([*ioValue floatValue] < 0.0) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidAgeCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Age cannot be negative" }];
        }
        return NO;
    }
    return YES;
}

Describing Property Relationships

类描述提供了一种描述类中一对一和一对多属性的方法. 定义类属性之间的这些关系可以通过键值编码对这些属性进行更智能,更灵活的操作.

Class Descriptions

NSClassDescription是一个基类, 它提供用于获取有关类的元数据的接口. 类描述对象记录特定类的对象的可用属性, 以及该类的对象与其他对象之间的关系(一对一,一对多和逆关系). 例如attributeKeys方法返回为一个类定义的所有属性的列表, toManyRelationshipKeystoOneRelationshipKeys方法返回定义一对多和一对一关系的键数组. inverseRelationshipKey:返回从提供的键的关系的目标指向接收者的关系的名称

NSClassDescription没有定义用于定义关系的方法. 具体的子类必须定义这些方法. 创建后可以使用NSClassDescriptionregisterClassDescription:forClass:类方法注册类描述

NSScriptClassDescriptionCocoa中提供的NSClassDescription的唯一具体子类. 它封装了应用程序的脚本信息

注意 详情可查看GUNStep NSClassDescription.h文件

Designing for Performance

键值编码是高效的, 尤其是依靠默认实现来完成大部分工作时, 但它确实增加了一个隔离级别(a level of indirection),该级别比直接方法调用要慢一些. 仅当可以从键值编码中受益时才使用键值编码,或者仅让使用的对象参与依赖键值编码的Cocoa技术

Overriding Key-Value Coding Methods

通常通过确保对象键值编码是从NSObject继承而来的然后按照本书中的描述提供特定于属性的访问器和相关方法以使它们符合键值编码. 一般很少需要重写键值编码访问器的默认实现, 例如valueForKey:setValue:forKey:或基于键的验证方法, 例如validateValue:forKey:. 因为这些实现缓存有关运行时环境的信息以提高效率, 所以如果重写它们以引入自定义逻辑, 请确保在返回前在超类中调用默认实现

Optimizing To-Many Relationships

当实现一对多关系时, 访问器的索引形式在许多情况下可显着提高性能尤其是对于可变集合而言

Compliance Checklist

请遵循本节中概述的步骤以确保对象符合键值编码. 有关详细信息请参见前面的部分

Attribute and To-One Relationship Compliance

对于作为属性或一对一关系的每个属性

  • 实现名为<key>is<Key>的方法, 或者创建名为<key>_<key>的实例变量. 允许编译器自动综合属性时编译器通常会执行此操作

如果属性是可变的

  • 实现set<Key>:方法. 允许编译器自动综合属性时编译器通常会执行此操作
  • 如果属性是标量, 实现setNilValueForKey:方法, 应对设置nil的情况

Indexed To-Many Relationship Compliance

对于有序, 一对多关系的每个属性

  • 实现名为<key>的方法, 或者有一个数组实例变量, 名字为<key>_<key>
  • 可选, 实现countOf<Key>方法, 或者实现objectIn<Key>AtIndex:<key>AtIndexes:
  • 选择性实现get<Key>:range:提高性能

另外如果属性是可变的

  • 实现insertObject:in<Key>AtIndex:insert<Key>:atIndexes:一个或者全部
  • 实现removeObjectFrom<Key>AtIndex:remove<Key>AtIndexes:.一个或者全部
  • 选择性实现replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:提高性能

Unordered To-Many Relationship Compliance

对于是无序的一对多关系的每个属性

  • 实现<key>方法返回一个set或者返回一个名为<key>_<key> NSSet的实例变量. 编译器通常会在自动合成属性时为您执行此操作
  • 选择性实现countOf<Key>, enumeratorOf<Key>, memberOf<Key>:方法

另外如果属性是可变的

  • 实现add<Key>Object: add<Key>:其一或者全部方法
  • 选择性实现intersect<Key>:以提高性能

Validation

选择验证需要的属性

  • 实现validate<Key>:error:方法

理解如有错误 望指正 转载请说明出处