文章目录
官方文档
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
一般来说,编译器会自动生成set
和get
方法,例如:
[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.name
,KVC
会先找到class
的类,看其是否遵守NSKeyValueCoding
,然后就是调用class
的valueForKey:name
了。不能找到,则会接收valueForUndefinedKey:
dictionaryWithValuesForKeys:
该方法为数组中的每个对象调用valueForKey
。返回的结果以NSDictionary
形式,包含数组中所有键的值。
setter
-
setValue:forKey:
为key
设置value
,当value
为NSNumber
或者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:
有其获取顺序,我们常见的UIView
有hidden
和isHidden
两个属性,他们都对应同一个值,下面讲述下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:
andmutableArrayValueForKeyPath:
These return a proxy object that behaves like an
NSMutableArray
object. -
mutableSetValueForKey:
andmutableSetValueForKeyPath:
These return a proxy object that behaves like an
NSMutableSet
object. -
mutableOrderedSetValueForKey:
andmutableOrderedSetValueForKeyPath:
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
?
答案是都可以
-
以
get<Key>
、<Key>
、is<Key>
或_< Key>
等名称搜索找到的第一个访问器实例方法
。如果找到了,调用它并继续执行步骤5
。否则继续下一步 -
如果没有上面的方法,则在实例中搜索
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);
-
如果没有找到简单的访问器方法或数组访问方法组,则查找名为
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
(对应于NSSet类定义的基本方法)的三种方法。如果找到所有这三个方法,创建一个集合代理对象来响应所有
NSSet
方法并返回它。否则,继续步骤4。这个代理对象随后将它接收到的任何NSSet消息转换成
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
。实际上,与键值编码兼容对象一起工作的代理对象允许底层属性的行为就像它是一个NSSet
一样,即使它不是。 -
如果没有找到简单的访问器方法或集合访问方法组,并且如果接收方的类方法
accessInstanceVariablesDirectly
返回YES
,则搜索一个名为_<key>
,_is< key>
,<key>
,或is<key>
的实例变量,按这个顺序。如果找到,直接获取实例变量的值,并继续执行步骤5。否则,继续步骤6。 -
如果检索到的属性值是对象指针,只需返回结果。
如果该值是
NSNumber
支持的标量类型,则将其存储在NSNumber
实例中并返回该值。如果结果是
NSNumber
不支持的标量类型,则转换为NSValue
对象并返回它。 -
如果其他方法都失败了,调用
valueForUndefinedKey:
。默认情况下,这将引发异常,但是NSObject
的子类可能提供特定于键的行为。
setValue:forKey:流程
- 首先找
set<Key>:
or_set<Key>
- 如果没有找到,
accessInstanceVariablesDirectly
returnsYES
,在实例中寻找像_<key>
,_is<Key>
,<key>
, oris<Key>
- 没有找到,触发
setValue:forUndefinedKey:
搜索集合的模式
集合的搜索
方式主要是找到insertObject:in<Key>AtIndex:
and removeObjectFrom<Key>AtIndex:
,即找到有没有操作该集合的方法。
并不是很常用。