1. 前言
我们都知道Objecttive-C对象能够通过Accessor Method直接访问对象,比如你有一个如下一个类名为TestClass,其有一个公开的接口属性:str。
@interface TestClass : NSObject
@property (strong) NSString *str;
@end
在编译时,编译器会为该属性默认合成Accessor Method。一个set方法如下声明所示:
- (void)setStr:(NSString *)string;
也有一个get的Accessor Method。你可以通过如下方式获取属性str的值:A 为TestClass的一个实例。
1. A.str;
2. [A str];
上述的Accessor Method访问属性的方式为直接访问,而Key-Value Coding为访问属性提供了一种间接访问属性的方式。 Key-Value Coding是一种名为NSKeyValueCoding的非正式协议启用的一种机制,符合该机制的对象可以对属性进行间接访问。一个类别如果间接或直接继承至NSObject那么,那就可以使用这种机制来间接访问对象的属性(通过编译器默认实现的必要方法)。有时候,Key-Value Coding能够简化你的代码。 使用符合Key-Value Coding的对象,可以做到如下几点:
- Access Object properties
访问属性可以通过
valueForKey:
,设置属性可以通过setValue:forKey:
。比如上面提到的TestClass,它的对象可以通过:其中forKey:
的参数是一个NSString \*
类型。这个参数(Key)是属性名称的。
//设置值
[A setValue:@"newString" forKey:@"str"];
//获取值
[A valueForKey:@"str"];
- Manipulate collection properties
- Invoke collection operators on collection objects
- Access non-object properties
- Access properties by key path
2. Key-Value的基本内容
2.1 Accessing Object Properties
一个对象通常它的属性定义在它的interface declaration中,可以将属性分为如下几种:
-
Attribute 一些scalars,strings,boolean value。NSNumber 和 其它不可变(NSColor)对象一般可看作是Attribute。
-
To-one relationships 单个对象,其本身不可变。看一个类的结构:类BankAccount有一个属性owner,类型为Person。类Person有一个属性address,类型为NSString。owner指向Person对象,也就是owner本身不变,但是Person对象中address的值可以变。
3. To-many relationships 指的是集合对象,比如NSArray或者NSSet。自定义的集合类也属于这种类型。
如下是三种类型的属性的例子:
@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.2 Identifying an Object’s Properties with Keys and Key Paths
在Key-Value Coding机制中,使用一个key(string type
)来识别一个属性。一般就是属性的名称(ASCII encoding,一般来说不包括空格且以小写字符开头)。
下面的代码就是给BanckAccount类的属性currentBalance
赋值的方式。
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
key-path可以理解为属性的路径,比如owner.address.street
,这就是key-path的形式。根据属性链string
来标记一个属性的key。比如为了给owner中的address的street属性赋值(Person和Address都要符合key-value coding),可以使key-path。类的结构可用下图表示:
[myAccount setValue:@"成华大道二仙桥" forKey:@"owner.address"];
2.3 Getting Attribute Values Using Keys
可以使用key来获取属性的值,有如下三种方法。
valueForKey:
valueForPath:
dictionaryWithValuesForKeys:
其中1和2需要一个类型为string
的参数,valueForPath:
需要的key-path如上文提到一样这里就不再详细介绍。主要介绍第三种dictionaryWithValuesForKeys:
。其定义如下:keys是一个NSArry类型,其内部元素都为NSString,也即是key。返回的是一个字典,key为你传递参数中的key,value为对应的valueForKey:
获取到的值。如果没有对应key的值为nil。但是在返回的字典中的value为NSNull。 (Note:如果没有找到对应key,则会valueForUndefinedKey:会抛出一个NSUndefinedKeyException
)
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
keys:An array containing `NSString` objects that identify properties of the receiver.
return value:
A dictionary containing as keys the property names in `keys`, with corresponding values being the corresponding property values.
如果在key-path中除了最后一个key的,其它key的对应属性的类别是To-Many relationships。则返回值value为一个集合。比如下面一个例子则返回一个数组,数组元素为transactions中所有交易的金额,因为transactions是一个集合类型。
[myAccount valueForPath:@"accounts.transactions.payee"];
2.4 Setting Attribute Values Using Keys
给属性设置值也有如下三种方法:
setValue:forKey
setValue:forKeyPath
setValuesForKeysWithDictionary:
这里主要介绍第三种方式,其它两种与上文提到的使用key和key-path一样,其参数是一个字典,字典的key值是属性的key,value是想要设定的值。当你想要设定为nil时,则用NSNull替代。如果设定的key不存在,则执行setValue:forUndefinedKey:
方法,会抛出NSUndefinedKeyException异常。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
keyedValues:A dictionary whose keys identify properties in the receiver. The values of the properties in the receiver are set to the corresponding values in the dictionary.
当你想设定一个不是对象类型的属性的值为nil,如CGFloat,那么它会给自己发送一个名为setNilValueForKey:
的消息,该消息的默认行为是抛出NSInvalidArgumentException。
2.5 Implementation of data source method without or with key-value coding
从下面一个例子可以看出,使用key-value coding的方式能够简化代码。
- without key-value coding
- (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;
}
- with key-value coding
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}
3. Accessing Collection Properties
为了访问获得属性为集合类型,你可以使用上述的get value 和 set value等方法。但是,如果你想操作集合的内容,你可以使用下面介绍的proxy object
。它的本质就是通过返回的mutable,对其进行增删,然后反应到原来的属性上。不要通过valueForKey:
获得一个immutable对象,然后利用其内容创建一个mutable对象,对其进行修改,在利用setValue:forKey
重新设值。当你使用下面描述的方法获得一个mutable object,对其进行增删,会将操作结果映射到原集合属性上。在大多数情况下,它比直接使用一个mutable property更有效。
- 返回
NSMutableArray
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- 返回NSMutableSet
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- 返回NSMutableOrdered
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
4. Using Collection Operators
在上面提到的valueForKeyPath:
,Path参数中可以携带operator key。以@开头。也就是operator key作为Path的一部分。这样说可能比较模糊,我们来看下一个包含operator key的Path是什么样子。
其中包含三部分,中间部分是以@
符号开头的Collection Operator
。其中Left Key Path
是一个keyPathToCollection
,作为path的一部分。根据前文提到的key path,就是指该属性类型为To-Many relationships
。而Collection Operator
的右边的就是keyPathToProperty
。当你对一个collection对象发送valueForKeyPath:消息时,比如NSArray,那么Left Key Path可以省略。比如下面:
@interface Person : NSObject
@property (strong) NSString *name;
@property (strong) NSString *city;
@property (strong) NSNumber *age;
@end
@interface MyClass : NSObject
@property (strong) NSArray <Person *> *owner;
@end
当你创建一个MyClass对象后:testObject.owner是一个指向NSArray对象的指针,也就是collection对象。
MyClass testObject = [[MyClass alloc] init];
[testObject.owner valueForKey:"@avg.age"];
所有的collection operator都需要一个Right key path,但是@count不需要。它仅仅是计算作为Left Key Path的collection对象中的元素个数。
collection operator包括如下三种类型的运算:
- Aggregation Operators 以某种方式聚集一个集合对象,并且返回的是一个单一对象。该对象通常与right key path中定义的属性的数据类型相同,但是@count是个例外,它不需要right key path,返回的一个类型为NSNumber。(@avg,@count,@max,@min,@sum)
- Array Operators 返回一个NSArray的对象。(@distinctUnionOfObjects, @unionOfObjects)
- Nesting Operators 处理一些集合中的元素是集合对象的集合对象。并根据collection operator返回是NSArray或NSSet的对象。(@distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets)
5. Representing Non-Object Values
当你使用valueForKey:
获取值,返回的是对象类型。使用setValue:ForKey:
设置值时,Value参数应该是对象类型。
比如你有一个类:其中有两个属性,一个时numFloat为非对象类型,number为对象类型。
@interface MyClass : NSObject
@property (assign) CGFloat numFloat;
@property (strong) NSNumber *number;
当你使用一个非对象类型的值给number属性设置值时,如下所示:它会将23.0 封装成一个 NSNumber对象。在将其赋值给属性number。
[MyClassObject setValue:23.0 forKey:@"numver"];
当你使用valueForKey:
获取一个非对象类型的属性值时,如下:因为numFloat是一个非对象类型,那么将会利用该值初始化一个NSNumber对象,并将其返回。
[MyClassObject valueForKey:@"numFloat"];
5.1 Wrapping and Unwrapping Scalar Types
下面是NSNumber与非对象类型的转换方法。
5.2 Wrapping and Unwrapping Structures
下面是一些通用的NSPoint,NSRange,NSRect和NSSize与非对象类型之间进行的转换。
当然也包括你自定义的Struct类型,一般是以NSValue进行转换。下面看一个简单的例子:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
使用MyClass的实例myClass,来获取属性threeFloats的值:
NSValue* result = [myClass valueForKey:@"threeFloats"];
默认的valueForKey方法,调用threeFloats的getter,并且讲返回结果转换成NSValue对象。 同样你可以设置threeFloats的值:
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
默认实现的setValue:ForKey:
先用getValue:
将类型为NSValue的value转换成ThreeFloats类型,返回一个struct对象,然后使用setThreeFloats:
方法进行设置值。
6. Validating Properties
Key-Value Coding也定义一些验证属性的方法。当你使用key-value coding中定义的方法去访问或者写一个属性值的话,可以对其进行验证。有如下几个方法:
- (BOOL)validateValue:(inout id _Nullable *)ioValue
forKey:(NSString *)inKey
error:(out NSError * _Nullable *)outError;
- (BOOL)validateValue:(inout id _Nullable *)ioValue
forKeyPath:(NSString *)inKeyPath
error:(out NSError * _Nullable *)outError;
如果该对象没有这些方法,默认验证成功,返回YES。
如果该对象有这些方法,那么返回值根据方法的是否验证成功决定。
验证方法需要value值以及error参数,那么调用该方法有三种结果。
- 验证方法认为值对象有效,不更改值或error参数,并且返回YES。
- 验证方法认为值对象无效,但是选择不改变它,在这种情况下,返回NO。并且将error参数的值被设置为错误的原因。
- 验证方法认为值对象无效,但是会创建一个有效对象进行替换,在这种情况下,返回YES。同时error参数不变。在该方法返回之前,该方法修改值引用,指向新的对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的。
下面来看一个具体的例子:
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
也有Automatic Validation的方法。但是这里不介绍,可以参考其他文档。
7. Accessor Search Patterns
最后来介绍一下当你调用setValue:forKey:
和valueForKey:
时访问器方法搜索模式是怎样的,但是我目前了解太少,这一部分暂时就不写了。等我了解够多,再来补上。
Thanks!