内省 Introspection

555 阅读5分钟

内省是一个面向对象语言的强大功能,这在 Objective-CCocoa 中也不例外。内省是指反映对象在运行时所具备的能力。比如对象在继承树中的位置,对象是否遵循某个协议又或者能否响应一个特定的消息。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 的子类,来调用 NSDatabyteslength 方法。 假设 itemNSMutableData 的实例对象, 那么 isKindOfClass:isMemberOfClass: 不同之处马上就能体现出来。将上述代码中的 isKindOfClass: 替换为 isMemberOfClass:,那么if条件将不成立,代码也无法执行。因为 itemNSMutableData 的实例,而不是 NSData 的实例,尽管 NSMutableDataNSData 的子类。

方法实现和遵守协议

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

对象比较

严格意义上来说,这些方法并不是内省方法,但是 hashisEqual: 方法却起着相似的作用。它们是用于识别和比较对象所必须的运行时工具。但是,它们不向运行时查询有关对象的信息,而是依赖于特定于类的比较逻辑。

hashisEqual: 都是 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,它包含两个属性,namedata。这两个属性具有相同的值,才能判断两个实例对象相等。

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:,比较对象的 namedata 是否相同。

在Cocoa框架的所有 isEqual: 方法中,参数传递 nil 会引起异常,但为了向后兼容,Cocoa 框架在接收到nil 参数后将返回NO。

原文地址