iOS学习笔记-KVC技术

官方文档

KeyValueCoding

KVC技术我们开发中经常涉及,但往往容易忽略很多部分,这里整理列出。

获取对象属性

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

  • 属性
  • 一个关系
  • 多个关系

下面的BankAccount展示了这3中类型

@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

一般来说,编译器会自动生成setget方法,例如:

[myAccount setCurrentBalance:@(100.0)];

这种访问是直接的,缺乏灵活性。符合键值对编码(key-value coding)的对象,提供了使用字符串标识符访问对象属性的更通用的机制。

Key和Key Paths标识对象属性

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

通过KVC技术我们可以这样访问其属性:

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

可以通过keyPath路径形式,访问其owner属性下的变量。keyPath是一串点分隔的Key,用于指定要遍历的对象属性序列。序列中第一个键的属性相对于接收器,随后的每个键相对于前一个属性的值进行计算。

Swift中,您可以使用#keyPath表达式,而不是使用字符串来表示键或键路径。


NSKeyValueCoding

继承自NSObject的对象会自动采用此协议。

getter

至少实现以下基本getter方法:

  • valueForKey:。如果不能键命名的属性,那么对象将向自己发送一个valueForUndefinedKey:

valueForUndefinedKey:的默认实现会引发NSUndefinedKeyException,但是子类可能会覆盖此行为并更优雅地处理这种情况。

  • valueForKeyPath:,例如path = class.nameKVC会先找到class的类,看其是否遵守NSKeyValueCoding,然后就是调用classvalueForKey:name了。不能找到,则会接收valueForUndefinedKey:
  • dictionaryWithValuesForKeys:该方法为数组中的每个对象调用valueForKey。返回的结果以NSDictionary形式,包含数组中所有键的值。

setter

  • setValue:forKey:key设置value,当valueNSNumber或者NSValue时,将自动解包,并将值付给属性。如果没有相关key,则会执行setValue:forUndefinedKey:,以NSUndefinedKeyException错误崩溃。可以子类重写拦截。

  • setValue:forKeyPath: 同样会根据path调用首个对象的setValue:forKey:

  • setValuesForKeysWithDictionary: 为每个key 触发 setValue:forKey:, 值得注意的是需要将nil替换为NSNull

集合对象(如NSArray、NSSet和NSDictionary)不能包含nil作为值。相反,使用NSNull对象表示nil值。NSNull提供一个表示对象属性的nil值的实例。dictionaryWithValuesForKeys: 以及相关的setValuesForKeysWithDictionary:在NSNull(在dictionary参数中)和nil(在存储属性中)之间做了自动转换。

valueForKey:有其获取顺序,我们常见的UIViewhiddenisHidden两个属性,他们都对应同一个值,下面讲述下KVC的查找过程

KVC的运用举例

对于Mac OS开发的我,之前一直有一个疑问,为什么NSTableView会有这样一个回调方法:

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

这是一般来说我们的逻辑,但是使用了KVC之后:

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

豁然开朗,以identifier对应key,可以很方便的映射。

集合类型KVC使用

有3中集合类型返回,分别是NSMutableArray,NSMutableSet,NSMutableOrderedSet

  • mutableArrayValueForKey: and mutableArrayValueForKeyPath:

    These return a proxy object that behaves like an NSMutableArray object.

  • mutableSetValueForKey:and mutableSetValueForKeyPath:

    These return a proxy object that behaves like anNSMutableSetobject.

  • mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:

    These return a proxy object that behaves like an NSMutableOrderedSet object.

这个有什么作用呢,是这样的,如果你声明了一个不可变数组

@property (nonatomic, strong) NSArray <NSString*> *mystrings;

你需要向里面加入元素,是不是会创建一个可变数组包含其内容,再添加,再进行赋值。

这样并不高效,KVC提供了一个代理对象,来让你能方便的处理这种情况。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.mystrings = @[];
    
    NSMutableArray *proxyArr = [self mutableArrayValueForKey:@"mystrings"];
    [proxyArr addObject:@"be strong ,be lier"];
    
    for (NSString* str in proxyArr) {
        NSLog(@"%@",str);
    }
}

本来是不可变的,通过KVC获取的代理对象是可变的,我们可以方便添加删除。

值得注意的是KVC能取到readonly的属性,那么我们同样也是可以处理的!

KVC集合操作符的使用

比如一个数组,里面有一个对象,对象有一个age属性,如何取得这个数组所有对象的age和或者平均数呢,KVC集合操作符提供了方便调用方式。

格式为 左键 集合操作 右键
在这里插入图片描述
例如有一个对象:

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

假设一个数组里面,存在多份Transaction,我们称之为self.transactions

在这里插入图片描述
我们取amount平均数等值:

//平均数 @avg
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
//元素总数 @count - 不需要右键
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
//最大值 @max
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
//最小值 @min
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
//某值总和 @sum
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
//返回集合对象 @distinctUnionOfObjects - 不会重复
//这里结果为 Car Loan, General Cable, Animal Hospital, Green Power, Mortgage.
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
//返回集合对象 @unionOfObjects - 会按顺序重复
//返回 Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital.
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

当数组里面包含数组时的嵌套情况:

//arrayOfArrays 包含了两个数组 - 两个数组分别包含对象
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
//返回两个数组中不重复的对象数组 - @distinctUnionOfArrays
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];

//返回两个数组中不重复的对象数组 - @unionOfArrays
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

//@distinctUnionOfSets 也是和 @distinctUnionOfArrays 类似的效果,只是用于集合set,然后返回的是集合类型。

非Object的值存入

官方文档

这里MacOS平台注意BOOL类型,注意文档中的说明。

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


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

valueForKey 的搜索的流程

  • 若一个类有实例变量NSString *_foo,调用setValue:forKey:时,可以以foo还是_foo作为key
    答案是 都可以
  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一样,即使它不是NSArray

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end

例如上面这个类,我需要取其pens,并不存在于属性中

	LGPerson *p = [LGPerson new];
    p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
    NSArray *arr = [p valueForKey:@"pens"]; // 动态成员变量
    NSLog(@"pens = %@", arr);

在其类中实现如下

// 个数
- (NSUInteger)countOfPens {
    return [self.penArr count];
}

 获取值
 1. (id) objectInPensAtIndex:(NSUInteger)index {
    return [NSString stringWithFormat:@"pens %lu", index];
}

则下段代码能够输出

	LGPerson *p = [LGPerson new];
    p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
    NSArray *arr = [p valueForKey:@"pens"]; // 动态成员变量
    NSLog(@"pens = %@", arr);
  1. 如果没有找到简单的访问器方法或数组访问方法组,则查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>:(对应于NSSet类定义的基本方法)的三种方法。

    如果找到所有这三个方法,创建一个集合代理对象来响应所有NSSet方法并返回它。否则,继续步骤4。

    这个代理对象随后将它接收到的任何NSSet消息转换成countOf<Key>enumeratorOf<Key>memberOf<Key>:。实际上,与键值编码兼容对象一起工作的代理对象允许底层属性的行为就像它是一个NSSet一样,即使它不是。

  2. 如果没有找到简单的访问器方法或集合访问方法组,并且如果接收方的类方法accessInstanceVariablesDirectly返回YES,则搜索一个名为_<key>_is< key><key>,或is<key>的实例变量,按这个顺序。如果找到,直接获取实例变量的值,并继续执行步骤5。否则,继续步骤6。

  3. 如果检索到的属性值是对象指针,只需返回结果。

    如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该值。

    如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回它。

  4. 如果其他方法都失败了,调用valueForUndefinedKey:。默认情况下,这将引发异常,但是NSObject的子类可能提供特定于键的行为。

setValue:forKey:流程

  1. 首先找set<Key>: or _set<Key>
  2. 如果没有找到, accessInstanceVariablesDirectly returns YES,在实例中寻找像 _<key>, _is<Key>, <key>, or is<Key>
  3. 没有找到,触发setValue:forUndefinedKey:

搜索集合的模式

集合的搜索方式主要是找到insertObject:in<Key>AtIndex: and removeObjectFrom<Key>AtIndex: ,即找到有没有操作该集合的方法。

具体的查看文档: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html#//apple_ref/doc/uid/20000955-CJBBBFFA

并不是很常用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值