键值编码编程指南(Key-Value Coding Programming Guide)

开始

关于键值编码

键值编码是一种机制,通过NSKeyValueCoding非正式协议,对象采用这种机制提供对其属性的间接访问。当对象符合键值编码时, 它的属性可以使用字符串参数通过简明、统一的消息接口进行寻址。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

通常使用访问器方法获取对对象属性的访问权限。get 访问器 (或 getter) 返回属性的值。set 访问器 (或 setter) 设置属性的值。在Objective-C中, 还可以直接访问属性内部的实例变量。以上述任何一种方式访问对象属性都非常直截了当, 但需要调用属性特定的方法或变量名。随着属性列表的增长或更改, 必须编写访问这些属性的代码。相反, 键值编码兼容对象提供了一个在其所有属性中一致的简单消息传递接口。

键值编码是许多其他Cocoa技术的基础概念, 如KVOCocoa bindingsCore DataAppleScript-ability。在某些情况下, 键值编码还可以帮助简化代码。

使用键值编码兼容对象

对象通常采用键值编码, 当它们 (直接或间接) 继承NSObject时, 它们都采用了NSKeyValueCodin协议, 并为基本方法提供默认实现。此类对象使其他对象能够通过简洁的消息接口执行以下操作:

  • 访问对象属性. 协议中指定的方法, 例如一般的 getter valueForKey:和常用的setter setValue:forKey:, 用于通过名称或键访问对象属性 (参数化为字符串)。这些方法和相关方法的默认实现使用key来定位基础数据并与之交互, 详见访问对象属性.

  • 操作集合属性. 配合对象集合属性(如NSArray对象)使用的访问器方法的默认实现与对象其他属性的访问器方法一样。此外, 如果对象定义了属性的集合访问器方法, 则它允许对集合的内容进行键值访问。这通常比直接访问效率更高, 并允许你通过标准化的接口处理自定义集合对象, 详见访问集合属性.

  • 在集合对象上调用集合运算符. 在键值编码兼容对象中访问集合属性时, 可以将集合运算符插入到键字符串中, 详见使用集合运算符。集合运算符指示NSKeyValueCoding getter 默认的实现去对集合执行某些操作, 然后返回一个全新的、过滤过的集合版本或单个表示某些集合特性的值 。

  • 访问非对象属性. 协议的默认实现支持检测非对象属性, 包括标量和结构, 并自动将它们包装成对象和解包,详见表示非对象值.此外, 该协议还声明了一种方法, 允许兼容对象通过键值编码接口在非对象属性上设置nil值时为该情况提供适当的操作。

  • 按键路径访问属性. 当你有一个键值编码兼容的对象的层级结构,你可以使用key path based方法在单次调用中获取或设置层次结构深处的值。

为对象采用键值编码

为了使您自己的对象兼容键值编码, 您可以确保它们遵循NSKeyValueCoding非正式协议并实现相应的方法, 如valueForKey:作为一般的 getter 和setValue:forKey:作为一般的 setter。幸运的是, 如上文所述, NSObject遵循此协议, 并为这些和其他基本方法提供了默认实现。因此, 如果从NSObject (或它的许多子类) 中派生对象, 则大部分工作已经完成。

为了使默认方法执行其工作, 您可以确保对象的访问器方法和实例变量遵守某些定义良好的模式。这允许默认实现在响应键值编码消息时查找对象的属性。然后, 您可以通过提供验证方法和处理某些特殊情况来扩展和自定义键值编码。

Swift中的键值编码

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

虽然键值编码协议方法直截了当地翻译成Swift, 但本指南主要侧重于Objective-C, 在这里您需要做更多的事来确保兼容, 而键值编码通常最有用。在整个指南中都需要注意到在 Swift 中采取明显不同方法的情况。

有关使用 swift 与Cocoa技术的详细信息, 请参阅Using Swift with Cocoa and Objective-C (Swift 4.1)。有关 swift 的完整说明, 请阅读The Swift Programming Language (Swift 4.1).

其他依赖于键值编码的Cocoa技术

键值编码兼容的对象可以广泛的应用于依赖这种访问的Cocoa技术, 其中包括:

  • 键值观察. 此机制使对象可以注册由其他对象属性中的更改驱动的异步通知, 详见Key-Value Observing Programming Guide.

  • Cocoa bindings. 此技术集完全实现了模型-视图-控制器范式, 其中模型封装应用程序数据、视图显示和编辑数据以及控制器在两者之间进行协调。阅读Cocoa Bindings Programming Topics了解有关Cocoa Bindings的更多信息。

  • Core Data. 此框架为与对象生命周期和对象图管理 (包括持久性) 相关的常见任务提供了通用和自动解决方案。您可以在Core Data Programming Guide中阅读有关Core Data的信息。.

  • AppleScript 这种脚本语言可以直接控制可脚本化的应用程序和 macOS 的许多部分。Cocoa的脚本支持利用键值编码来获取和设置可脚本化对象中的信息。NSScriptKeyValueCoding非正式协议中的方法为使用键值编码提供了额外的功能, 包括按多值键中的索引获取和设置键值, 并将键值强制 (或转换) 为适当的数据类型。AppleScript Overview提供了 AppleScript 及其相关技术的高级别概述。

键值编码基本原理

访问对象属性

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

  • 属性. 一些简单的值, 例如标量(scalars)、字符串或布尔值。值对象(如NSNumber)和其他不可变类型(如NSColor) 也被视为属性。
  • 对一关系. 指具有自身属性的可变对象。对象的属性可以在对象本身没有更改的情况下进行更改。
  • 对多关系. 指集合对象。你通常使用NSArrayNSSet的实例来保存此类集合, 自定义集合类也是可行的。

清单 2-1中声明的BankAccount对象演示了每种类型的属性。

清单 2-1BankAccount对象的属性

@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

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

[myAccount setCurrentBalance:@(100.0)];

这很直接, 但缺乏灵活性。另一方面, 一个键值编码兼容对象提供了一个更通用的机制来使用字符串标识符访问对象的属性。

使用键和键路径标识对象的属性

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

因为清单 2-1中的BankAccount类是符合键值编码的, 所以它能识别键(即其属性的名称)ownercurrentBalancetransactions 。您可以通过其键来设置值, 而不是调用setCurrentBalance:方法:

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

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

键路径Key path是一个用点操作符.来分隔键的字符串, 用于指定要遍历的对象属性序列。序列中第一个键的属性相对于接收者, 每个后续键相对于上一个属性的值进行计算。键路径对于使用单个方法深入调用对象的层次结构很有用。

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

注意
在 Swift 中, 您可以使用#keyPath表达式, 而不是使用字符串来指示键或键路径。这提供了编译期间检查的优点, 详见Using Swift with Cocoa and Objective-C (Swift 4.1) 中的Keys and Key Paths章节。

使用键获取属性值

当对象遵循 NSKeyValueCoding非正式协议,则该对象支持键值编码。从NSObject(提供了协议中基本方法的默认实现)继承的对象, 它会自动采用此协议的某些默认行为。这样的对象至少实现以下基本的key-based的 getter:

  • valueForKey: 返回由键参数命名的属性的值。如果根据访问器搜索模式中描述的规则无法找到由键命名的属性, 则该对象将自己发送valueForUndefinedKey:消息。valueForUndefinedKey的默认实现将抛出NSUndefinedKeyException异常, 但子类可以重写此行为并更优雅地处理此情况。

  • valueForKeyPath: 返回相对于接收者的指定键路径的值。键路径序列中指定键所对应的对象如果不是键值编码兼容的, 即valueForKey:的默认实现无法找到访问器方法-那么就会接收valueForUndefinedKey:消息。

  • dictionaryWithValuesForKeys: 返回相对于接收者的一组键所对应的值。该方法为数组中的每个键调用valueForKey: 。返回的NSDictionary包含数组中所有键的值。

注意
集合对象 (如NSArrayNSSetNSDictionary) 不能包含nil作为值。而是使用NSNull对象表示nil值。NSNull提供一个表示对象属性的nil值的单个实例。dictionaryWithValuesForKeys:的默认实现和相关的setValuesForKeysWithDictionary:会在NSNull (在字典参数中) 和nil(在存储的属性中)之间进行自动转换 。

当您使用键路径来寻址属性时, 如果键路径中的最后一个键是一对多关系 (即引用集合), 则返回的值是一个集合, 其中包含对多关系的键右侧的键的所有值。例如, 请求键路径 "transactions.payee" 的值返回包含所有transaction对象中payee对象的数组。这也适用于键路径中的多个数组。键路径accounts.transactions.payee返回包含所有帐户中所有交易记录的所有收款人对象的数组。

使用键设置属性值

getter一样, 键值编码兼容对象还提供了一小组具有默认行为的广义 setter, 它基于在NSObject中对NSKeyValueCoding协议的实现:

  • setValue:forKey: 设置相对于接收到给定值消息的对象的指定键的值。setValue:forKey:的默认实现会自动对表示标量和结构的NSNumberNSValue对象执行unwarp操作,并将它们设置到相应的属性中。有关warpunwarp的详细信息, 详见表示非对象值

如果接收setter调用的对象中没有对应指定键的属性,该对象将自己发送一个[setValue:forUndefinedKey:]消息。setValue:forUndefinedKey:的默认实现将抛出NSUndefinedKeyException异常。但是, 子类可以重写此方法以自定义方式处理请求。

  • setValue:forKeyPath: 在相对于接收者的指定键路径上设置给定值。键路径序列中指定键所对应的对象如果不是键值编码兼容的,将会收到setValue:forUndefinedKey:消息。

  • setValuesForKeysWithDictionary: 将指定字典中的值设置到接收者的属性中, 使用字典键标识属性。默认实现调用每个键值对的setValue:forKey: , 根据需要用nil替换NSNull对象。

在默认实现中, 当您尝试将非对象属性设置为nil值时, 键值编码兼容对象将自己发送一个setNilValueForKey:消息。setNilValueForKey:的默认实现将抛出[NSInvalidArgumentException]异常, 但对象可能会重写此行为以替换默认值或标记值, 详见处理非对象值

使用键简化对象访问

想知道基于键的 gettersetter 如何简化代码, 请查看下面的示例。在 macOS 中, NSTableViewNSOutlineView对象将标识符字符串与每列关联起来。如果表的模型对象不是符合键值编码的, 则表的数据源方法将强制检查每个列标识符, 依次查找要返回的正确属性, 如清单 2-2所示。此外, 在将来, 当您向模型中添加另一个属性时, 在本例中为Person 对象, 还必须重新访问数据源方法, 添加另一个条件来测试新属性并返回相关值.

清单 2-2不基于键值编码的数据源方法的实现

- (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;
}

另一方面,清单 2-3展示了相同数据源的方法的一个更紧凑的实现, 该数据源方法使用的是键值编码兼容的Person对象。仅使用valueForKey: getter, 数据源方法将使用列标识符作为键返回适当的值。除了更短的时间外, 它还更通用, 因为在以后添加新列时, 只要列标识符始终与模型对象的属性名称匹配, 它就会继续保持不变。

清单 2-3基于键值编码的数据源方法的实现

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

访问集合属性

键值编码兼容对象以与公开其他属性相同的方式公开其对多属性。您可以像使用valueForKey:setValue:forKey: (或它们的键路径等同方法) 一样获取或设置集合对象。但是, 当您要操作这些集合的内容时, 使用协议定义的可变代理方法通常是最有效的。

该协议为集合对象访问定义了三种不同的代理方法, 每个都具有一个键和一个键路径变体方法:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    这些方法返回的代理对象的行为类似于NSMutableArray对象。

  • mutableSetValueForKey:mutableSetValueForKeyPath:

    这些方法返回的代理对象的行为类似于NSMutableSet对象。

  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:

    这些方法返回的代理对象的行为类似于NSMutableOrderedSet对象。

当您对代理对象进行操作、向其添加对象、从其中移除对象或替换其中的对象时, 协议的默认实现将相应地修改基础属性。这比使用valueForKey:得到一个不可变集合对象,创建一个修改了内容的可变集合对象, 然后将其存储回带有setValue:forKey:消息的对象更有效。在许多情况下, 它也比直接使用可变属性更有效。这些方法提供了对集合对象中保存的对象保持键值观察遵从性的额外好处 (请参见Key-Value Observing Programming Guide以了解详细信息)。

使用集合运算符

当你发送向键值编码兼容的对象发送valueForKeyPath:消息时, 可以在键路径中嵌入集合运算符。集合运算符是前面有 at 符号 (@) 的一小部分关键字列表, 它指定了getter在返回之前以某种方式操作数据。NSObject提供的valueForKeyPath:的默认实现实现了此行为。

当键路径包含集合运算符时, 运算符前面的键路径的任何部分 (称为左键路径) 指示相对于消息接收者需要去操作的集合。如果将消息直接发送到集合对象 (如NSArray实例), 则可以省略左键路径。

图 4-1运算符键路径格式

image.png

集合运算符展示了三种基本行为类型:

  • 聚合运算符以某种方式合并集合的对象, 并返回一个通常与在右键路径中命名的属性的数据类型相匹配的单个对象。@count运算符是一个特例,它没有右键路径, 并且始终返回NSNumber实例。

  • 数组运算符返回一个NSArray实例, 其中包含命名集合中保存的对象的某些子集。

  • 嵌套运算符处理包含其他集合的集合, 并返回一个NSArrayNSSet实例 (根据运算符), 它将嵌套集合的对象以某种方式组合在一起。

示例数据

下面的说明包括演示如何调用每个运算符的代码段以及这样做的结果。这依赖于BankAccount类 (在[列表 2-1]中显示), 它包含Transaction对象的数组。其中每一个都代表一个简单的checkbook条目, 如清单 4-1中所声明的那样。

清单 4-1Transaction对象的接口声明

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

为了进行讨论, 假定BankAccount实例具有一个事务数组, 其中填充了表 4-1中显示的数据, 并使示例从BankAccount对象内部调用。

表 4-1Transactions对象的示例数据

payee 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

聚合运算符

聚合运算符处理arrayset属性, 生成一个反映集合的某些方面的单个值。

@avg

当指定@avg运算符时, valueForKeyPath:读取集合中每个元素的右键路径指定的属性, 将其转换为double (nil值用0替代), 并计算算术平均值。然后返回存储在NSNumber实例中的结果。

获取表 4-1中示例数据之间的平均交易记录金额:

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

transactionAverage的格式化的结果为 $ 456.54。

@count

指定@count运算符时, valueForKeyPath:返回一个包含集合中的对象个数的NSNumber实例。右键路径 (如果存在) 将被忽略。

在transactions中获取Transaction对象的数目:

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

numberOfTransactions的值为13。

@max

指定@max运算符时, valueForKeyPath:在由右键路径命名的集合项之间进行搜索, 并返回最大值。搜索使用compare:方法进行比较, 许多基础类 (如NSNumber类) 中都有定义。因此, 由右键路径指示的属性必须持有对此消息有意义响应的对象。搜索忽略值为nil的集合项。

在表 4-1中列出的交易记录中, 获取日期值 (即最新交易记录的日期) 的最大数量:

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];

latestDate的值为 Jul 15, 2016.

@min

指定@min运算符时, valueForKeyPath:在由右键路径命名的集合项之间进行搜索, 并返回最小值。搜索使用compare:方法进行比较, 许多基础类 (如NSNumber类) 中都有定义。因此, 由右键路径指示的属性必须持有对此消息有意义响应的对象。搜索忽略值为nil的集合项。

在表 4-1中列出的事务中, 获取日期值 (即最早的事务的日期) 的最短时间。

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

earliestDate的值为 Dec 1, 2015.

@sum

指定@sum运算符时, valueForKeyPath:读取集合中每个元素的右键路径指定的属性, 将其转换为double (nil值替换为 0), 并计算总和。然后返回存储在NSNumber实例中的结果。

获取表 4-1中示例数据之间的交易记录金额的总和:

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

amountSum的结果为 $ 5935.00。

数组运算符

数组运算符使valueForKeyPath:返回与右键路径指示的特定对象集相对应的对象数组。

重要
在使用数组运算符时, 如果有任何叶(leaf)对象为nil, 则valueForKeyPath:方法将引发异常。

@distinctUnionOfObjects

指定@distinctUnionOfObjects运算符时, valueForKeyPath:将创建并返回一个数组, 其中包含与右键路径指定的属性对应的集合的不同对象。

获取transactions中的交易记录的payee属性值的集合, 但省略了重复值:

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

生成的distinctPayees数组包含以下每一个字符串实例:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage

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

@unionOfObjects

指定@unionOfObjects运算符时, valueForKeyPath:将创建并返回一个数组, 其中包含与由右键路径指定的属性对应的集合的所有对象。与@distinctUnionOfObjects不同, 不删除重复对象。

获取transactions中的交易记录的payee属性值的集合:

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.记录了重复值。

注意
@distinctUnionOfArrays运算符类似, 但移除重复对象。

嵌套运算符

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

重要
如果在使用嵌套运算符时, 有任何叶(leaf)对象为nil, 则valueForKeyPath:方法将引发异常。

对于下面的说明, 请看第二个称为moreTransactions的数据数组, 其中填充了表 4-2中的数据, 并与原来的transactions数组一起插入嵌套数组:

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

表 4-2 moreTransactions数组中假设的Transaction数据

payee 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 Sho $600.00 Jun 14, 2016
@distinctUnionOfArrays

指定@distinctUnionOfArrays运算符时, valueForKeyPath:创建并返回一个数组, 其中包含与右键路径指定的属性相对应的所有集合的组合的不同对象。.

arrayOfArrays中的所有数组中获取payee属性的不同值:

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:创建并返回一个数组, 其中包含与由右键路径指定的属性相对应的所有集合的组合的所有对象, 而不删除重复项。

arrayOfArrays中的所有数组中获取payee属性的值:

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

注意
@distinctUnionOfArrays运算符类似, 但移除重复对象。

@distinctUnionOfSets

当指定@distinctUnionOfSets运算符时, valueForKeyPath:创建并返回一个NSSet对象, 其中包含与由右键路径所指定的属性相对应的所有集合组合的不同对象。

此运算符的行为与@distinctUnionOfArrays类似, 只是它需要一个NSSet实例, 其中包含对象的NSSet实例, 而不是NSArray实例中包含NSArray实例。此外, 它还返回一个NSSet实例。假设示例数据已存储在集合而不是数组中, 则示例调用和结果与@distinctUnionOfArrays中显示的相同。.

验证属性

键值编码协议定义了支持属性验证的方法。正如使用基于键的访问器读取和写入键值编码兼容对象的属性一样, 也可以按键 (或键路径) 验证属性。当您调用validateValue:forKey:error:(或validateValue:forKeyPath:error:) 方法时, 协议的默认实现将搜索接收验证消息的对象 (或在键路径的末尾的对象), 该方法的名称与模式validate<Key>:error:相匹配。如果对象没有此类方法, 则默认情况下验证成功, 默认实现返回YES.当存在属性特定的验证方法时, 默认实现将返回调用该方法的结果。

注意
您通常仅在Objective-C中使用此处描述的验证。在 Swift 中, 通过依赖 optionals 和强类型检查的编译器支持, 可以更便捷地处理属性验证, 同时使用内置的 willSet 和 didSet 属性观察器来测试任何运行时 API 协定, 详见
The Swift Programming Language (Swift 4.1)
Property Observers章节对willSet didSet的描述。

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

  1. 验证方法认为值对象有效并返回YES而不改变值或错误。
  2. 验证方法认为值对象无效, 但选择不更改它。在这种情况下, 该方法返回NO并将错误引用 (如果调用方提供) 设置为指示失败原因的NSError对象。
  3. 验证方法认为值对象无效, 但创建一个新的、有效的替换项。在这种情况下, 该方法返回YES同时使错误对象不被触及。返回之前, 该方法修改值引用以指向新值对象。当它进行修改时, 该方法总是创建一个新对象, 而不是修改旧值, 即使 value 对象是可变的。

清单 6-1显示了如何调用name字符串的验证的示例。

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

自动验证

通常, 键值编码协议及其默认实现都不定义自动执行验证的任何机制。相反, 您可以在您的应用程序中使用适合的验证方法。

某些其他Cocoa技术在某些情况下会自动进行验证。例如, 当保存托管对象上下文时, Core Data自动执行验证 (详见Core Data Programming Guide)。此外, 在 macOS 中, Cocoa Bindings 允许您指定验证是否自动发生 (请阅读Cocoa Bindings Programming Topics了解有关Cocoa Bindings的更多信息。)。

访问器搜索模式

NSObject提供的NSKeyValueCoding协议的默认实现将基于键的访问器调用映射到对象的基础属性, 使用一组明确定义的规则。这些协议方法使用键参数搜索其自己的对象实例, 以查找访问器、实例变量以及遵循某些命名约定的相关方法。尽管您很少修改此默认搜索, 但了解它的工作原理, 对于跟踪键值编码对象的行为以及使您自己的对象兼容是很有帮助的。

注意
本节中的描述使用<key ><key >作为在一个键值编码协议方法中出现的作为参数的键串的占位符,然后该方法被用作二次方法调用或变量名称查找的一部分。映射的属性名称遵循占位符的情况。例如, 对于 getter <key>is<Key>, 名为 "hidden" 的属性映射为hiddenisHidden.

基本的Getter搜索模式

valueForKey:的默认实现, 给定一个key参数作为输入, 执行以下过程, 从接收valueForKey调用的类实例中进行操作:。

  1. 在实例中搜索找到第一个访问器方法, 其名称类似于get<Key><key>is<Key>_<key>, 按此顺序。如果找到, 则调用它, 然后继续执行步骤5以得到结果。否则继续执行下一步。

  2. 如果找不到简单访问器方法, 则在实例中搜索其名称与模式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的子类可能会提供特定于键的行为。

基本的Setter搜索模式

setValue:forKey:的默认实现, 给定keyvalue参数作为输入, 在接收调用的对象内尝试将名为key的属性设置为value(或者, 对于非对象属性, 则为unwarp value, 详见表示非对象值) , 使用以下过程:

  1. 按照顺序查找第一个名为set<Key>:_set<Key>的访问器。如果找到, 用输入值 (或根据需要unwrap value) 调用它, 然后完成。
  2. 如果找不到简单访问器, 并且类方法accessInstanceVariablesDirectly返回YES, 则查找具有_<key>、 _is<Key>、 <key>或is<Key>的名称的实例变量, 按该排列顺序。如果找到, 则直接使用输入值 (或unwrap value) 设置变量并完成。
    在找不到访问器或实例变量时, 调用setValue:forUndefinedKey:。默认情况下, 这会引发异常, 但NSObject的子类可能会提供特定于键的行为。

可变数组的搜索模式

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

  1. 查找一对方法,如insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex: (对应于NSMutableArray的基本方法方法insertObject:atIndex:removeObjectAtIndex: ), 或insert<Key>:atIndexes:remove<Key>AtIndexes: (对应于NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:方法)。

    如果对象有至少一个插入方法和至少一个删除方法, 则返回一个代理对象,该代理对象响应NSMutableArray消息,并将insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:, and remove<Key>AtIndexes:消息的一些组合发送给mutableArrayValueForKey:的原始接受者。

    当接收mutableArrayValueForKey:消息的对象还实现一个可选的替换对象方法, 其名称类似于replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:, 代理对象在适合最佳性能时也会利用这些功能。

  2. 如果对象没有可变数组方法, 则改用其名称与模式set<Key>:相匹配的访问器方法。在这种情况下, 通过向mutableArrayValueForKey:的原始接收者发出set<Key>:消息, 返回响应NSMutableArray消息的代理对象。

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

  1. 如果未找到可变数组方法或访问器, 并且接收者的类响应YES对于accessInstanceVariablesDirectly, 则按照该顺序搜索具有_<key><key>等名称的实例变量。

    如果找到了这样的实例变量, 则返回一个代理对象, 它将接收到的每个NSMutableArray消息转发给实例变量的值, 这通常是NSMutableArray或其子类的实例。

  2. 如果所有其他操作失败, 则返回一个可变的集合代理对象, 它将发出setValue:forUndefinedKey:消息到 mutableArrayValueForKey 的原始接收者, 每当它收到NSMutableArray`消息。

    setValue:forUndefinedKey:的默认实现会抛出NSUndefinedKeyException异常, 但子类可能会重写此行为。

推荐阅读更多精彩内容