内省是一个面向对象语言的强大功能,这在 Objective-C 和 Cocoa 中也不例外。内省是指反映对象在运行时所具备的能力。比如对象在继承树中的位置,对象是否遵循某个协议又或者能否响应一个特定的消息。NSObject 协议和类定义了很多内省方法,可用于 runtime 时期判断对象特征。
谨慎使用,内省使得面向对象编程更加的有效和健壮,它可以帮助你避免给对象发送错误的消息,比如当对象之间比较相同时。接着将会向你展示如何有效地使用 NSObject 的内省方法。
判断继承关系
一旦你知道对象的类型,就会得到很多关于对象的信息。你就能知道对象拥有哪些属性和方法,还有它可以响应哪些方法,甚至就算是不熟悉这个类,通过内省也可以确定它可以响应哪些消息。
NSObject 协议声明了几个确定对象在继承树中位置的方法,这些方法适用于不同的场合。例如,类和父类的实例方法将分别返回接收者的类和父类的 class 对象,这些方法要求您将一个Class对象与另一个Class对象进行比较。清单4-1给出了一个简单的使用示例。
**Listing 4-1 ** Using the class and superclass methods
while(id anObject = [objectEnumerator nextObject]) {
if ([self class] == [super class]) {
// do somethings appropriates...
}
}
有时候,可以使用
class或者superclass来获取合适的消息接收者
在实际开发中,更加常见内省方法是检查对象的所属类,比如isKindOfClass:或者isMemberOfClass:。前者是用来判断对象是否为指定类型或者继承自该类型的实例。isMemberOfClass:方法,可用于判断实例对象是否为指定的类型;isKindOfClass:更加的常用,通过这个方法你可以找到实例对象能响应的所有方法。
Listing 4-2 Using isKindOfClass:
// 假设item是NSMutableData的实例对象
// 如果将isKindOfClass替换为isMemberOfClass,那么条件将永不成立
if ([item isKindOfClass:[NSData class]]) {
const usigned char *bytes = [item bytes];
usigned int length = [item length];
// ...
}
上述代码通过判断对象 item 是否为 NSData 的子类,来调用 NSData 的 bytes 和 length 方法。
假设 item 是 NSMutableData 的实例对象, 那么 isKindOfClass: 和 isMemberOfClass: 不同之处马上就能体现出来。将上述代码中的 isKindOfClass: 替换为 isMemberOfClass:,那么if条件将不成立,代码也无法执行。因为 item 是 NSMutableData 的实例,而不是 NSData 的实例,尽管 NSMutableData 是 NSData 的子类。
方法实现和遵守协议
NSObject 中还有两个更加有用的方法:respondsToSelector: 和 conformsToProtocol:。前者的作用是判断对象是否实现了指定的方法,而后者的作用是判断对象是否遵守了指定的协议。
在下面的情况下可以使用这两个方法,有一些匿名对象,在向其发送消息时,需要先检查它是否具有响应特定消息的能力,可以避免一些运行时错误
Listing 4-3 illustrates how you might use the respondsToSelector: method in your code.
- (void)doCommandBySelector:(SEL)aSelector {
if ([self respondsToSelector:aSelector]) {
[self performSelector:aSelector withObject:nil];
} else {
[_client doCommandBySelector:aSelector];
}
}
Listing4-4 illustrates how you might use the conformsToProtocol: method in your code
// ...
if (!([((id)testObject) conformsToProtocol:@protocol(NSMenuItem)])) {
NSLog(@"Custom MenuItem, '%@', not loaded; it must conform to the
'NSMenuItem' protocol.\n", [testObject class]);
[testObject release];
testObject = nil;
}
对象比较
严格意义上来说,这些方法并不是内省方法,但是 hash 和isEqual: 方法却起着相似的作用。它们是用于识别和比较对象所必须的运行时工具。但是,它们不向运行时查询有关对象的信息,而是依赖于特定于类的比较逻辑。
hash 和 isEqual: 都是 NSObject 中的协议方法,它们的关系极为密切。 hash 方法的实现必须返回一个可以作为哈希表结构中表地址的整数。如果两个对象相同(由 isEqual: 方法决定),则它们必须具有相同的哈希值。如果你有一个 NSSet 的对象集合,你需要定义 hash 并验证不变量,它们需要返回相同的值。 NSObject 中的 isEqual: 默认实现仅检查指针是否相等。
使用 isEqual: 非常简单,它将消息接收者和参数进行比较。对象比较非常的常见,在运行时通过对象比较执行不同的代码。如清单4-5所示,您可以 isEqual: 用来决定是否执行某项操作,在这种情况下,可以保存已被修改的用户首选项。
Listing 4-5 Using isEqual:
- (void)saveDefaults {
NSDictionary *prefs = [self preferences];
if (![origValues isEqual:prefs])
[Preferences savePreferencesToDefaults:prefs];
}
现在有一个 NSObject 的子类,需要自定义的相等规则,此时需要重写 isEqual:,子类定义了一个额外的属性,两个实例中的该属性的值相同,才能被视为同一个对象。例如创建了 NSObject 的子类 MyWidget,它包含两个属性,name 和 data。这两个属性具有相同的值,才能判断两个实例对象相等。
Listing 4-6 Overriding isEqual:
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return [self isEqualToWidget:other];
}
- (BOOL)isEqualToWidget:(MYWidget *)aWidget {
if (self == aWidget)
return YES;
if (![(id)[self name] isEqual:[aWidget name]])
return NO;
if (![(id)[self data] isEqual:[aWidget data]])
return NO;
return YES;
}
isEqual: 先判断指针是否相同,然后是类型是否满足,最后调用了子类的比较器,比较器将参数类型进行强制转换,这是Cocoa的常规做法。比如 NSString 类的 isEqualToString: 方法和 NSTimeZone 类的 isEqualToTimeZone: 方法,这里自定义了比较器 isEqualToWidget:,比较对象的 name 和 data 是否相同。
在Cocoa框架的所有 isEqual: 方法中,参数传递 nil 会引起异常,但为了向后兼容,Cocoa 框架在接收到nil 参数后将返回NO。