Objective-C之Key-Value Coding

540 阅读9分钟

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的对象,可以做到如下几点:

  1. Access Object properties 访问属性可以通过valueForKey:,设置属性可以通过setValue:forKey:。比如上面提到的TestClass,它的对象可以通过:其中forKey:的参数是一个NSString \*类型。这个参数(Key)是属性名称的。
//设置值
[A setValue:@"newString" forKey:@"str"];
//获取值
[A valueForKey:@"str"];
  1. Manipulate collection properties
  2. Invoke collection operators on collection objects
  3. Access non-object properties
  4. Access properties by key path

2. Key-Value的基本内容

2.1 Accessing Object Properties

一个对象通常它的属性定义在它的interface declaration中,可以将属性分为如下几种:

  1. Attribute 一些scalars,strings,boolean value。NSNumber 和 其它不可变(NSColor)对象一般可看作是Attribute。

  2. To-one relationships 单个对象,其本身不可变。看一个类的结构:类BankAccount有一个属性owner,类型为Person。类Person有一个属性address,类型为NSString。owner指向Person对象,也就是owner本身不变,但是Person对象中address的值可以变。

未命名文件 (1).png 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。类的结构可用下图表示:

未命名文件 (2).png

[myAccount setValue:@"成华大道二仙桥" forKey:@"owner.address"];

2.3 Getting Attribute Values Using Keys

可以使用key来获取属性的值,有如下三种方法。

  1. valueForKey:
  2. valueForPath:
  3. 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

给属性设置值也有如下三种方法:

  1. setValue:forKey
  2. setValue:forKeyPath
  3. 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的方式能够简化代码。

  1. 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;
}
  1. 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更有效。

  1. 返回NSMutableArray
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
  1. 返回NSMutableSet
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
  1. 返回NSMutableOrdered
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;

4. Using Collection Operators

在上面提到的valueForKeyPath:,Path参数中可以携带operator key。以@开头。也就是operator key作为Path的一部分。这样说可能比较模糊,我们来看下一个包含operator key的Path是什么样子。

未命名文件 (3).png 其中包含三部分,中间部分是以@符号开头的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包括如下三种类型的运算:

  1. Aggregation Operators 以某种方式聚集一个集合对象,并且返回的是一个单一对象。该对象通常与right key path中定义的属性的数据类型相同,但是@count是个例外,它不需要right key path,返回的一个类型为NSNumber。(@avg,@count,@max,@min,@sum)
  2. Array Operators 返回一个NSArray的对象。(@distinctUnionOfObjects, @unionOfObjects)
  3. 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与非对象类型的转换方法。

截屏2022-01-05 22.07.46.png

5.2 Wrapping and Unwrapping Structures

下面是一些通用的NSPoint,NSRange,NSRect和NSSize与非对象类型之间进行的转换。

截屏2022-01-05 22.11.07.png

当然也包括你自定义的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参数,那么调用该方法有三种结果。

  1. 验证方法认为值对象有效,不更改值或error参数,并且返回YES。
  2. 验证方法认为值对象无效,但是选择不改变它,在这种情况下,返回NO。并且将error参数的值被设置为错误的原因。
  3. 验证方法认为值对象无效,但是会创建一个有效对象进行替换,在这种情况下,返回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!