KVC

关于键值编码

键值编码(KVC)是一种由NSKeyValueCoding非正式协议提供的机制,对象采用该机制来提供对其属性的间接访问。当对象兼容键值编码时,可以使用简洁、统一的接口和字符串参数来访问其属性。这种间接访问机制补充了实例变量和其关联的访问器方法提供的直接访问。

通常情况下,使用访问器方法来访问对象的属性。get访问器(或者getter)返回属性的值,set访问器(或者setter)设置属性的值。在Objective-C中,还可以直接访问属性的实例变量。以任何方式访问对象属性都很简单,但需要调用属性特定的方法或者变量名称。随着属性列表的增长或改变,访问这些属性的代码也必须如此。 相反,兼容键值编码的对象提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。

键值编码是一个基本概念,是许多其他Cocoa技术的基础,例如KVO、Cocoa绑定、Core Data和AppleScript-ability。在某些情况下,键值编码还有助于简化代码。

访问对象属性

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

  • Attributes:这些是简单值,例如标量、字符串和或者布尔值。诸如NSNumber之类的值对象和诸如NSColor之类的其他不可变类型也被视为属性。
  • To-one relationships:这些是具有自己属性的可变对象,对象的属性可以在对象本身不变的情况下更改。例如,一个BankAccount对象可能具有一个owner属性,该属性是People对象的实例,owner属性本身具有一个address属性。在BankAccount对象对owner属性的引用不变的情况下,owneraddress属性可能会更改。换句话说,银行账户的所有者没有变更,但所有者的地址可能变了。
  • To-many relationships:这些是集合对象。通常是NSArray或者NSSet的实例,也可以是自定义集合类。

以下代码中声明的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

为了保持封装性,对象通常为其接口上的属性提供访问器方法。可以显式地编写这些方法,也可以依赖编译器自动合成它们。无论哪种方式,必须在编译之前将属性名称写入代码中。例如,前面声明的BankAccount对象,编译器会合成一个可以给myAccount实例调用的setter:

[myAccount setCurrentBalance:@(100.0)];

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

使用键(Key)和键路径(Key Path)标识对象的属性

键是标识特定属性的字符串。按照惯例,表示属性的键是代码中显示的属性本身的名称。键必须使用ASCII编码,不能包含空格,并且通常以小写字母开头。

由于BankAccount类是兼容键值编码的,所以它可以识别键ownercurrentBalancetransactions,它们是其属性的名称。可以通过键代替调用setCurrentBalance:方法为currentBalance属性设置值:

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

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

键路径是一个使用点分割多个键的字符串,用于指定要遍历的对象属性序列。序列中第一个键的属性是相对于接收者的,并且后面的键都是相对于其前面一个键所表示的属性。键路径对于使用单个方法深入到对象层次结构是非常有用的。

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

使用键获取属性值

当对象遵循NSKeyValueCoding协议时,其是兼容键值编码的。继承自NSObject(其提供了NSKeyValueCoding协议的必要方法的默认实现)的对象会自动采用此协议的某些默认行为。这样的对象至少实现了以下基础的基于键的getter:

  • valueForKey::返回接收者的与指定键对应的属性的值。如果根据访问器查找方式中描述的规则无法找到key所指定的属性,则该对象会向自身发送valueForUndefinedKey:消息。valueForUndefinedKey:方法的默认实现会抛出一个NSUndefinedKeyException,但是子类可以覆盖此行为并更优雅地处理该情况。
  • valueForKeyPath::返回相对于接收者的指定键路径对应的属性的值。键路径序列中的任一对象不能兼容特定键的键值编码——即其valueForKey:方法的默认实现无法找到访问器方法——该对象会接收到一个valueForUndefinedKey:消息。
  • dictionaryWithValuesForKeys::返回接收者的与键数组中每个键对应的属性的值。该方法为数组中的每个键调用valueForKey:方法,返回的NSDictionary包含数组中所有键的值。

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

当使用键路径来寻址属性时,如果键路径中的最后一个键的前一个键是to-many relationship(即它引用一个集合),则返回的值是一个包含集合中的每个对象的最后一个键所标识属性的值的集合。例如,请求键路径transactions.payee会返回一个包含所有Transaction对象的payee实例的数组。这也适用于键路径中的多个数组。例如,键路径accounts.transactions.payee返回一个包含所有账户中所有交易的所有收款人对象的数组。

使用键设置属性值

与getter一样,兼容键值编码的对象也提供了一组具有默认行为的通用setter:

  • setValue:forKey::将消息接收者的与指定键对应的属性设置为给定值。setValue:forKey:方法的默认实现自动解包表示标量和结构体的NSNumberNSValue对象,并将它们分配给属性。有关包装和解包语义的详细信息,请参看表示非对象值。如果消息接收对象没有指定的键对应的属性,则该对象会向自身发送setValue:forUndefinedKey:消息。setValue:forUndefinedKey:方法的默认实现抛出一个NSUndefinedKeyException。但是,子类可以重写此方法以自定义方式处理请求。
  • setValue:forKeyPath::将相对于接收者的指定键路径对应的属性设置为给定值。键路径序列中的任何一个不兼容键值编码的对象会收到setValue:forUndefinedKey:消息。
  • setValuesForKeysWithDictionary::使用字典键标识属性,使用字典中的设置属性的值。其默认实现会为每个键值对调用setValue:forKey:方法,并根据需要使用nil替换NSNull对象。

在默认实现中,当试图将非对象属性设置为nil时,兼容键值编码的对象会向自身发送setNilValueForKey:方法。setNilValueForKey:方法的默认实现抛出一个NSInvalidArgumentException,但是对象可以覆盖此行为以替换默认值或标记值,如处理非对象值中所述。

使用键简化对象访问

要了解基于键的getter和setter如何简化代码,请考虑以下示例。 在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:方法的键参数来获取对应的属性值。除了更短之外,它还更通用,因为只要列标识符始终与模型对象的属性名称匹配,它在以后添加新列时将继续保持不变。

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

访问集合属性

兼容键值编码的对象以与公开其他类型属性相同的方式公开其To-many relationships类型的属性。可以使用valueForKey:setValue:forKey:方法来获取和设置集合对象,就像任何其他对象一样。但是,当想要操纵这些集合的内容时,使用协议定义的可变代理方法通常是最有效的。

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

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

当对代理对象执行向其添加对象和从中删除或者替换对象的操作时,协议的默认实现会相应地修改集合对象。这比使用valueForKey:方法获取不可变的集合对象,根据该集合对象创建一个包含已修改的内容的集合对象,然后使用setValue:forKey:方法将其存储回对象更加有效。在许多情况下,它也比直接使用可变集合属性更有效。这些方法为集合对象中保存的对象提供了保持KVO兼容的额外好处(有关详细信息,请参看KVO)。

使用集合运算符

当向兼容键值编码的对象发送valueForKeyPath:消息时,可以在键路径中嵌入一个集合运算符。集合运算符是一个开头是@符号的关键字,它告知getter在返回之前应该以某种方式操作数据。NSObject提供的valueForKeyPath:方法的默认实现实现了这种行为。

当键路径包含一个集合运算符时,在运算符之前的键路径部分(成为左键路径)表示valueForKeyPath消息的接收者对象的集合。如果将消息直接发送给集合对象(例如NSArray实例),则略去左键路径。

运算符之后的键路径部分(称为右键路径)指定运算符要操作的集合的属性。除@count之外的所有集合运算符都需要右键路径。下图说明了运算符键路径格式。

图2-1 运算符键路径格式.png

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

  • 聚合运算符以某种方式合并集合中的对象,并返回与右键路径中指定的属性的数据类型相匹配的单个对象。@count运算符是一个例外,其没有右键路径,并总是返回一个NSNumber实例。
  • 数组运算符返回一个包含指定的集合中所保存对象的子集的NSArray实例。
  • 嵌套运算符处理包含其他集合的集合,并返回一个NSArray或者NSSet实例,具体取决于运算符,它以某种方式合并嵌套集合中的对象。

样本数据

以下描述包括演示如何调用每个运算符的代码片段,以及执行此运算的结果。它们依赖于上文提到的BankAccount类,它包含一个含有Transaction对象的数组。每个Transaction对象代表一个简单的支票簿条目,如下所示。

@interface Transaction : NSObject

@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When

@end

为了讨论,假设现在有一个BankAccount实例有一个填充了下表所示数据的transactions数组。

payee values amount values formatted as currency date values formatted as month day, year
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

聚合运算符

聚合运算符可以处理数组或者属性集,生成反映集合的某些方面的单个值。

@avg

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

获取样本数据的平均交易金额:

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:方法进行比较,许多Foundation类定义了该方法,例如NSNumber类。因此,右键路径标识的属性必须持有一个能够对compare:消息进行响应的对象。查找会忽略值为nil的属性。

获取样本数据中Transaction对象的date属性的最大值:

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

latestDate的格式化值为Jul 15, 2016。

@min

当指定@min运算符时,valueForKeyPath:方法查找右键路径指定的集合元素的属性,并返回值最小的一个。查找时使用compare:方法进行比较,许多Foundation类定义了该方法,例如NSNumber类。因此,右键路径标识的属性必须持有一个能够对compare:消息进行响应的对象。查找会忽略值为nil的属性。

获取样本数据中Transaction对象的date属性值的最小值:

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

earliestDate的格式化值为Dec 1, 2015。

@sum

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

获取样本数据的交易总金额:

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

amountSum的格式化结果为$5,935.00。

数组运算符

数组运算符会使得valueForKeyPath:方法返回一个与右键路径标识的特定对象集对应的对象数组。

重要:如果使用数组运算符时,任何叶对象为nilvalueForKeyPath:方法会引发异常。

@distinctUnionOfObjects

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

获取样本数据中transactions数组中所有Transaction对象的不同的payee属性值的集合:

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

生成的distinctPayees数组包含Car Loan,General Cable,Animal Hospital,Green Power,Mortgage。

@unionOfObjects

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

获取样本数据中transactions数组中所有Transaction对象的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。

嵌套运算符

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

重要:如果使用嵌套运算符时,任何叶对象为nilvalueForKeyPath:方法会引发异常。

为方便描述,假设存在填充了以下数据的被称为moreTransactions的第二个数据数组,并于上文中起始的transactions数组一起收集到一个嵌套数组中:

NSArray* moreTransactions = @[transactionData];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
payee values amount values formatted as currency date values formatted as month day, year
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 Jul 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 - Cottage,General Cable - Cottage,General Cable - Cottage,Second Mortgage,Second Mortgage,Second Mortgage,Hobby Shop。

@distinctUnionOfSets

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

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

表示非对象值

NSObject提供的键值编码协议方法的实现同时支持对象属性和非对象属性。默认实现自动在对象参数或者返回值与非对象属性之间进行转换。这使得即使存储的属性是标量或者结构体,基于键的setter和getter的签名也保持一致。

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

类似地,默认情况下,setter(例如setValue:forKey:)在给定特定键时确定一个属性的访问器或者实例变量所需要的数据类型。如果数据类型不是对象类型,则setter首先向传入的值对象发送一个适当的<type>Value消息来提取基础数据,并存储该数据。

注意:当使用非对象属性的nil值调用键值编码协议setter的其中一个时,setter会向setter消息的接收对象发送一个setNilValueForKey:消息。该方法的默认实现会引发NSInvalidArgumentException异常,但子类可以覆盖此行为,如处理非对象值中所述,例如设置标记值或者提供有意义的默认值。

包装和解包标量类型

下表列出了默认的键值编码实现使用NSNumber实例包装的标量类型。对于每种数据类型,该表显示了用于将基础属性值初始化为一个NSNumber实例以作为getter返回值的创建方法,还显示了用于在设置操作期间从setter输入参数中提取值的访问器方法。

Data type Creation method Accessor method
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被定义为signed char类型,而KVC不会区分这点。因此,当key所标识的属性为BOOL类型时,不应该传递诸如@"ture"@"YES"这样的字符串作为value给setValue:forKey:方法。否则,由于BOOLchar类型,KVC将尝试调用charValue方法,但NSString没有实现此方法,这会导致运行时错误。取而代之的是,传递一个NSNumber对象,例如@(1)或者@(YES),作为setValue:forKey:的value参数。此限制不适用于iOS,iOS中的BOOL被定义为bool类型,而KVC调用boolValue方法,该方法适用于NSNumber对象或格式正确的NSString对象。

包装和解包结构体

下表显示了默认访问器用于包装和解包常见的NSPointNSRangeNSRectNSSize结构体的创建方法和访问器方法。

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

自动包装和解包并不只限于NSPointNSRangeNSRectNSSize,结构体类型可以包装在NSValue对象中,如下所示。

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface MyClass

@property (nonatomic) ThreeFloats threeFloats;

@end

使用MyClass类的实例时,可以使用键值编码获取threeFloats的值:

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

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

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

验证属性

键值编码协议定义了支持属性验证的方法。就像使用基于键的访问器来读取和写入兼容键值编码的对象的属性一样,也可以通过键(或键路径)来验证属性。当调用validateValue:forKey:error:方法(或者validateValue:forKeyPath:error:方法)时,协议的默认实现会在接收验证消息的对象(或者键路径标识的属性所属对象)中查找方法名称与格式validate<Key>:error:相匹配的方法。如果对象没有此类方法,则默认验证成功并且返回YES。当存在特定于属性的验证方法时,默认实现将返回调用该方法的结果。

注意:仅在Objective-C中使用属性验证。

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

  • 验证方法认为值对象有效并返回YES,且不会更改值对象或提示错误。
  • 验证方法认为值对象无效,但选择不更改它。 在这种情况下,该方法返回NO并将错误引用(如果调用者提供)指向一个NSError对象,该对象指示失败的原因。
  • 验证方法认为值对象无效,但会创建一个新的有效对象作为替补。 在这种情况下,该方法返回YES,同时将错误引用指向一个NSError对象。 在返回之前,该方法修改值引用以指向新的值对象。 当执行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的。

以下代码显示了如何为名称字符串调用验证的示例:

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绑定允许我们指定应自动执行验证(有关详细信息,请参看Cocoa Bindings Programming Topics)。

访问器查找方式

NSObject提供的NSKeyValueCoding协议的默认实现使用明确定义的规则集将基于键的访问器调用映射到对象的属性。这些协议方法使用键参数在对象中查找访问器、实例变量和遵循某些约定的相关方法。虽然很少需要修改此默认查找,但了解它的工作方式能够帮助我们跟踪键值编码对象的行为和使我们自己的对象兼容键值编码。

注意:本节中的描述使用<key><Key>作为在键值编码协议方法中的键字符串参数的占位符。协议方法将占位符用作辅助方法调用或变量名查找的一部分。映射的属性名称取决于占位符。例如,对于get访问器<key>is<Key>,名为hidden的属性映射到hiddenisHidden

Getter的查找方式

给定一个键参数作为输入,valueForKey:方法的默认实现会执行以下过程:

  1. valueForKey:消息的接收对象中按顺序依次查找名为get<Key><key>is<Key>或者_<key>的访问器方法。如果存在某个方法,则调用该方法并跳到第5步。否则,执行第2步。

  2. 查找名为countOf<Key>objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)和<key>AtIndexes:(对应于NSArrayobjectsAtIndexes:方法)的方法。如果找到第一个方法和其他两个方法中的至少一个,则创建一个能够响应NSArray类所有方法的集合代理对象,并返回该集合代理对象,查找完成。否则,执行第3步。

我们在操作该集合代理对象时,集合代理对象会将其接收的任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合并发送给valueForKey:消息的接收对象。如果接收对象还实现了名为get<Key>:range:的可选方法,则集合代理对象也会在适当时使用该方法。实际上,集合代理对象与兼容键值编码的对象一起工作,使得兼容键值编码的对象的集合属性的行为就像该属性是NSArray一样,即使它并不是。

  1. 查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>:(对应于NSSet类定义的原始方法)的三种方法。如果三种方法全部存在,则创建一个能够响应NSSet类所有方法的集合代理对象,并返回该集合代理对象,查找完成。否则执行第4步。

我们在操作该集合代理对象时,集合代理对象会将其接收的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合并发送给valueForKey:消息的接收对象。实际上,集合代理对象与兼容键值编码的对象一起工作,使得兼容键值编码的对象的集合属性的行为就像该属性是NSSet一样,即使它并不是。

  1. 如果没有找到访问器方法和集合访问方法组,并且valueForKey:消息的接收对象的类方法accessInstanceVariablesDirectly返回YES,则按顺序依次查找名为_<key>_is<Key><key>或者is<Key>的实例变量。如果存在,则直接获取实例变量的值并执行第5步。否则,执行第6步。

  2. 如果检索到的属性值是一个对象指针,则返回该结果,查找完成。
    如果属性值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该值,查找完成。
    如果属性值是NSNumber不支持的标量类型,则将其转换为NSValue对象并返回该值,查找完成。

  3. 如果以上所有查找都失败了,则调用valueForUndefinedKey:方法,查找完成。 默认情况下,这会引发异常,但NSObject的子类可能会提供特定于键的行为。

Setter的查找方式

给定键和值参数作为输入,setValue:forKey:方法的默认实现会执行以下过程:

  1. setValue:forKey:消息的接收对象中按顺序依次查找名为set<Key>:或者_set<Key>的访问器方法。如果存在某个方法,则使用输入的值调用该方法来设置属性值,查找完成。否则,执行第2步。

  2. 如果没有找到访问器方法,并且setValue:forKey:消息的接收对象的类方法accessInstanceVariablesDirectly方法返回YES,则按顺序依次查找名为_<key>_is<Key><key>或者is<Key>的实例变量。如果存在,则直接使用输入的值来设置实例变量,查找完成。否则,执行第3步。

  3. 如果没有找到访问器方法和实例变量,则调用setValue:forUndefinedKey:方法,查找完成。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于键的行为。

Mutable Array的查找方式

给定键和值参数作为输入,mutableArrayValueForKey:方法的默认实现为名称为<key>的集合属性返回一个可变代理数组,其会执行以下过程:

  1. mutableArrayValueForKey:消息的接收对象中查找一对名为insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(对应于NSMutableArray类的原始方法)的方法,或者一对名为insert<Key>:atIndexes:remove<Key>AtIndexes:(对应于NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:方法)的方法。如果至少存在一对插入和删除方法,则返回一个能够响应NSMutableArray消息的集合代理对象,查找完成。

我们在操作该集合代理对象时,集合代理对象会将接收到的NSMutableArray消息转换为insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes:消息的某种组合发送给mutableArrayValueForKey:消息的接收对象。当接收对象实现了一个可选的名为replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:的替换对象方法时,集合代理对象会在适当时间使用它们以获得最佳性能。

  1. 如果不存在一对插入和删除方法,会查找名为set<Key>:的访问器方法。如果存在该方法,则返回一个能够响应NSMutableArray消息的集合代理对象,查找完成。

该集合代理对象与第1步中返回的集合代理对象有所不同,其是通过发送set<Key>:消息给mutableArrayValueForKey:消息的接收对象来响应NSMutableArray消息的。

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

  1. 如果可变数组方法和访问器方法都不存在,并且mutableArrayValueForKey:消息的接收对象的类方法accessInstanceVariablesDirectly返回YES,则按顺序依次查找名为_<key>或者<key>的实例变量。如果存在实例变量,则返回一个集合代理对象,查找完成。

我们在操作该集合代理对象时,集合代理对象会将其接收到的所有NSMutableArray消息转发给实例变量,该实例变量通常是NSMutableArray或其子类之一。

  1. 如果以上所有步骤都失败,则返回一个可变集合代理对象,查找完成。

该集合代理对象在收到NSMutableArray的消息时,向mutableArrayValueForKey消息的接收对象发送一个setValue:forUndefinedKey:消息,查找完成。setValue:forUndefinedKey:的默认实现会引发一个NSUndefinedKeyException,但子类可能会覆盖此行为。

Mutable Ordered Set的查找方式

mutableOrderedSetValueForKey:方法的默认实现会识别与valueForKey:方法相同的访问器方法和mutable ordered set访问器方法,并遵循相同的直接访问实例变量策略。但是,其返回的是一个可变集合代理对象,而valueForKey:方法返回的是一个不可变集合代理对象。此外,它还执行以下操作:

  1. 在接收对象中查找一对名为insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(对应于NSMutableOrderedSet类定义的原始方法)的方法,或者一对名为insert<Key>:atIndexes:remove<Key>AtIndexes:(对应于NSMutableOrderedSet类的insertObjects:atIndexes:removeObjectsAtIndexes:)的方法。如果至少存在一对插入和删除方法,则返回一个能够响应NSMutableOrderedSet消息的可变集合代理对象,查找完成。

我们在操作该可变集合代理对象时,可变集合代理对象会将接收到的NSMutableOrderedSet消息转换为insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes:消息的某种组合发送给mutableOrderedSetValueForKey:消息的接收对象。当接收对象实现了一个可选的replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:的方法时,可变集合代理对象也会在适当时间使用它们。

  1. 如果未找到mutable ordered set方法,则查找名为set<Key>:的访问器。在这种情况下,会返回一个可变集合代理对象,查找完成。

该可变集合代理对象每次接收到NSMutableOrderedSet消息时,会发送一个set<Key>:消息给mutableOrderedSetValueForKey:消息的接收对象。

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

  1. 如果mutable ordered set方法和访问器方法都不存在,并且mutableOrderedSetValueForKey:消息的接收对象的类方法accessInstanceVariablesDirectly返回YES,则按顺序依次查找名为_<key>或者<key>的实例变量。如果存在实例变量,则返回一个可变集合代理对象,查找完成。

该可变集合代理对象会将其接收到的所有NSMutableOrderedSet消息转发给实例变量,该实例变量通常是NSMutableOrderedSet或其子类之一。

  1. 如果以上所有步骤都失败,则返回一个可变集合代理对象,查找完成。

该可变集合代理对象在收到NSMutableOrderedSet消息时,向mutableOrderedSetValueForKey:消息的接收对象发送一个setValue:forUndefinedKey:消息。setValue:forUndefinedKey:的默认实现会引发一个NSUndefinedKeyException,但子类可能会覆盖此行为。

Mutable Set的查找方式

给定一个键参数作为输入,mutableSetValueForKey:方法的默认实现为调用对象的名为<key>的集合属性返回一个可变集合代理对象,其会执行以下过程:

  1. 查找一对名为add<Key>Object:remove<Key>Object:的方法(对应于NSMutableSet的原始方法addObject:removeObject:),或者一对名为add<Key>:remove<Key>:的方法(对应于NSMutableSetunionSet:minusSet:方法)。如果至少存在一对插入和删除方法,则返回一个能够响应NSMutableSet消息的代理对象,查找完成。

我们在操作代理对象时,代理对象会将接收到的NSMutableSet消息转换为add<Key>Object:remove<Key>Object:addObject:removeObject:消息的某种组合发送给mutableSetValueForKey:消息的接收对象。当接收对象实现了一个名为intersect<Key>:或者set<Key>:的方法时,代理对象会在适当时间使用它们以获得最佳性能。

  1. 如果mutableSetValueForKey:消息的接收者是一个managed object,则查找模式不会像non-managed object那样继续。 有关详细信息,请参看Core Data Programming Guide

  2. 如果未找到mutable set方法,并且mutableSetValueForKey:消息的接收对象不是一个managed object,则会查找名为set<Key>:的访问器方法。如果存在该方法,则返回一个代理对象,查找完成。

该代理对象每次接收到NSMutableSet消息时,会向mutableSetValueForKey:消息的接收对象发送一个set<Key>:消息。

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

  1. 如果未找到mutable set方法和访问器方法,并且mutableSetValueForKey:消息的接收对象的类方法accessInstanceVariablesDirectly返回YES,则按顺序依次查找名为_<key>或者<key>的实例变量。如果存在实例变量,则返回一个代理对象,查找完成。

该代理对象会将其接收到的所有NSMutableSet消息转发给实例变量,该实例变量通常是NSMutableSet或其子类之一。

  1. 如果以上所有步骤都失败,则返回一个代理对象,查找完成。

该代理对象在收到NSMutableSet的消息时,向mutableSetValueForKey:消息的接收对象发送一个setValue:forUndefinedKey:消息。

实现基本的键值编码兼容

当对对象采用键值编码时,依赖于对象从NSObject类继承的NSKeyValueCoding协议的默认实现。反过来,默认实现依赖于我们根据某些明确的格式来定义对象的实例变量和访问器方法,以便在接收键值编码消息时,它可以将键字符串与属性相关联。

通常,通过使用@property语句声明属性并允许编译器自动合成实例变量(ivar)和访问器来遵循Objective-C中的标准格式。默认情况下,编译器遵循预期的格式。

如果需要在Objective-C中手动实现访问器或实例变量,请遵循本节中的指导原则来维护基本的兼容性。

基本的Getter

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

- (NSString*)title
{
    // Extra getter logic…

    return _title;
}

对于保存布尔值的属性,也可以使用前缀为is的方法,例如hidden布尔属性:

- (BOOL)isHidden
{
    // Extra getter logic…

    return _hidden;
}

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

基本的Setter

要实现存储属性值的setter,请使用名称为带有前缀set的首字母大写的属性名的方法。 对于hidden属性:

- (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,我们也可以提供上述方法覆盖。

实例变量

当某个键值编码访问器方法(例如valueForKey:)的默认实现无法查找到属性的访问器时,它会询问调用accessInstanceVariablesDirectly方法来询问该类是否允许直接使用实例变量。默认情况下,此类方法返回YES,但可以覆盖此方法返回NO

如果允许使用实例变量,请确保使用带下划线_前缀的属性名来命名它们。通常情况下,编译器在自动合成属性时会执行此操作。但如果使用显式的@synthesize指令,则可以手动执行这种命名操作。

@synthesize title = _title;

在某些情况下,会使用@dynamic指令告知编译器将在运行时提供getter和setter,而不是使用@synthesize或者允许编译器自动合成属性。可以通过这样做来避免自动合成setter,以便可以提供集合访问器,如定义集合方法中所述。在这种情况下,手动声明实例变量作为接口声明的一部分。

@interface MyObject : NSObject {
    NSString* _title;
}

@property (nonatomic) NSString* title;

@end

定义集合方法

当使用标准命名约定来创建访问器和实例变量时,键值编码协议的默认实现可以定位到它们以响应键值编码消息。对于表示to-many relationships(请看访问对象属性中的描述)的集合对象,情况和其他属性一样。但是,如果实现了集合访问器,而不是集合属性的基本访问器,则可以:

  • NSArrayNSSet以外的集合类建立to-many relationships。在对象中实现集合方法时,getter的默认实现返回一个代理对象,代理对象会调用这些集合方法来响应它接收到的NSArrayNSSet消息。底层集合属性不必是NSArrayNSSet的实例,因为代理对象会使用这些集合方法来提供预期的行为。
  • 在改变to-many relationships的内容时,提供更好的性能。协议的默认实现会使用集合方法来改变属性,而不是使用基本的setter重复创建新的集合对象来响应每个更改。
  • 为对象的集合属性的内容提供键值观察兼容的访问。有关键值观察的更多信息,请参看KVO

可以实现两类集合访问器中的一种,具体取决于是希望关系的行为类似于一个索引的、有序的集合(如NSArray对象),还是一个无序的、唯一的集合(如NSSet对象)。在任何一种情况下,都要至少实现一组方法来支持对属性的读取访问,然后额外添加一组方法来使集合的内容的突变成为可能。

注意:键值编码协议未声明本节中描述的方法。取而代之的是,NSObject提供的协议的默认实现会在兼容键值编码的对象中查找这些方法,如访问器查找方式中所述,并使用它们来处理键值编码消息。

访问索引集合

添加索引访问器方法来提供一个计算、检索、添加和替换有序关系中的对象的机制。底层集合属性通常是NSArray或者NSMutableArray的实例,但是如果提供集合访问器,则使得实现了这些方法的任何集合对象像数组一样被操作成为了可能。

索引集合Getter

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

注意:在现代Objective-C中,编译器默认为每个属性合成一个getter,因此默认实现不会使用本节中的方法创建只读代理(请查看Getter的查找方式)。 可以通过不声明属性(仅依赖于ivar)或将属性声明为@dynamic(告知编译器会在运行时提供访问器行为)来解决此问题。 无论哪种方式,编译器都不会提供默认的getter,并且默认实现会使用以下方法。

  • countOf<Key>:此方法将to-many relationship中的对象数作为NSUInteger返回,就像数组的原始方法count一样。实际上,当底层集合属性是一个NSArray时,使用count方法返回结果。
    例如,对于表示一个银行交易列表的to-many relationship,由名为为transactionsNSArray支持:
- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}
  • objectIn<Key>AtIndex:或者<key>AtIndexes::第一个方法返回to-many relationship中在指定的索引位置的对象,而第二个方法返回在由NSIndexSet参数指定的索引位置的对象数组。 它们分别对应于NSArray方法objectAtIndex:objectsAtIndexes:,只需要实现其中一个。 transactions数组的对应应方法为:
- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}

- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}
  • get<Key>:range::此方法是可选的,但可以提高性能。 它返回集合中处于指定范围内的对象,对应于NSArray方法getObjects:range:transactions数组的实现为:
- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}

索引集合Mutator

支持有序可变的to-many relationship需要实现不同的方法组。当提供这些setter方法时,默认实现在响应mutableArrayValueForKey:消息时,返回一个行为类似于NSMutableArray对象的代理对象,但该代理对象会使用mutableArrayValueForKey:消息的接收对象的方法来执行其工作。这通常比直接返回NSMutableArray对象更有效,它还使得to-many relationship的内容兼容键值观察成为可能。

为了使对象的键值编码兼容一个可变有序的to-many relationship,请实现以下方法:

  • insertObject:in<Key>AtIndex:或者insert<Key>:atIndexes::第一个方法接收要插入的对象和该对象的索引,第二个方法接收一个对象数组和包含对象数组中每个对象的索引的NSIndexSet对象,只需要其中一种方法。它们类似于NSMutableArrayinsertObject:atIndex:insertObjects:atIndexes:方法。
    transactions对象被声明为一个NSMutableArray
- (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:或者remove<Key>AtIndexes::第一个方法接收要删除的对象的索引,第二个方法接收一个包含要删除对象的索引的NSIndexSet对象,只需要其中一种方法。它们对应于NSMutableArrayremoveObjectAtIndex:removeObjectsAtIndexes:方法。
- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index {
    [self.transactions removeObjectAtIndex:index];
}

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self.transactions removeObjectsAtIndexes:indexes];
}
  • replaceObjectIn<Key>AtIndex:withObject:或者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];
}

访问无序集合

添加无序集合访问器方法,以提供一种访问和修改无序关系中的对象的机制。通常,底层集合属性是一个NSSet或者NSMutableSet实例。 但是,当集合属性实现了这些访问器时,使得对象像NSSet实例一样操作成为了可能。

无序集合Getter

当提供以下集合getter方法以返回集合中的对象数、迭代集合对象和测试对象是否已存在于集合中时,响应valueForKey消息的协议的默认实现返回一个行为类似于NSSet的代理对象,但其调用以下集合方法来完成其工作。

  • countOf<Key>:此方法返回集合中的对象数,对应于NSSetcount方法。当底层集合对象是NSSet时,直接调用此方法。例如,名为employeesNSSet对象包含Employee对象:
- (NSUInteger)countOfEmployees {
    return [self.employees count];
}
- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}
  • memberOf<Key>::此方法将传递的对象与集合中的内容进行比较,并将匹配对象作为参数返回。如果未找到匹配的对象,则返回nil。如果手动实现,通常使用isEqual:方法来比较对象。 当底层集合对象是NSSet对象时,可以使用等效的member:方法。
- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}

无序集合Mutator

支持无序可变的to-many relationship需要实现不同的方法组。当提供这些setter方法时,默认实现在响应mutableSetValueForKey:消息时,返回一个行为类似于NSMutableSet对象的代理对象,但该代理对象会使用mutableSetValueForKey:消息的接收对象的方法来执行其工作。这通常比直接返回NSMutableSet对象更有效,它还使得to-many relationship的内容兼容键值观察成为可能。

为了使对象的键值编码兼容一个可变无序的to-many relationship,请实现以下方法:

  • add<Key>Object:或者add<Key>::这些方法将一个或者一组对象添加到关系中,向关系添加一组项目时,请确保关系中不存在同样的对象。只需要其中一种方法,它们类似于NSMutableSetaddObject:unionSet:方法。对于employees集:
- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}

- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}
  • remove<Key>Object:或者remove<Key>::这些方法从关系中删除单个或者一组项目,只需要其中一种方法。它们类似于NSMutableSetremoveObject:minusSet:方法。例如:
- (void)removeEmployeesObject:(Employee *)anObject {
    [self.employees removeObject:anObject];
}

- (void)removeEmployees:(NSSet *)manyObjects {
    [self.employees minusSet:manyObjects];
}
  • intersect<Key>::此方法接收一个NSSet参数,从关系中删除所有不是输入集和集合集公共的对象。 这对应于NSMutableSetintersectSet:。例如:
- (void)intersectEmployees:(NSSet *)otherObjects {
    return [self.employees intersectSet:otherObjects];
}

处理非对象值

通常情况下,兼容键值编码的对象依赖于键值编码的默认实现来自动包装和解包非对象属性,如表示非对象值中所述。但是,可以覆盖此默认行为。

如果兼容键值编码的对象接收到一个将nil作为非对象属性的值的setValue:forKey:消息,setValue:forKey:的默认实现会发送一个setNilValueForKey:消息给该对象,该消息的默认实现会引发一个NSInvalidArgumentException异常。可以覆盖该方法来提供特定的行为。

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

注意:当一个对象覆盖了不推荐使用的unableToSetNilForKey:方法时,setValue:forKey:会调用该方法,而不是setNilValueForKey

添加验证

键值编码协议定义了通过键或者键路径来验证属性的方法,这些方法的默认实现依赖于我们定义的一些方法。具体来说,为任何想要验证的属性提供一个validate<Key>:error:方法。默认实现在响应validateValue:forKey:error:消息时会查找这些方法。

如果没有为属性提供验证方法,则协议的默认实现假定验证该属性成功而不管属性值是什么。

实现一个验证方法

当为属性提供一个验证方法时,该方法通过引用接收两个参数:要验证的值对象和用于返回错误信息的NSError。验证方法可以执行以下三种操作之一:

  • 当值对象有效时,返回YES,并且不更改值对象和错误。
  • 当值对象无效且不能或不想提供有效的替代方法时,请将error参数设置为NSError对象,该对象指示失败的原因并返回NO
  • 当值对象无效但我们知道有效替代项时,请创建有效对象,将值引用分配给新对象,并返回YES且不修改错误引用。 如果提供其他值,则始终返回新对象,而不是修改正在验证的对象,即使原始对象是可变的。
- (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;
}

重要:在修改错误引用之前,始终检查错误引用是否为NULL

标量值的验证

验证方法的value参数是一个对象,因此,非对象属性的值包装在NSNumber或者NSValue对象中。以下代码演示了标量属性age的验证方法。

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

推荐阅读更多精彩内容