13

有谁知道为什么 NextStep/Apple 决定在向 Nil 对象传递消息时采取什么都不做的“便捷方法”,而在将实例化对象传递给无效选择器时采用引发异常的“Java 方法”?

例如,

// This does "nothing"
NSObject *object = Nil;
[object thisDoesNothing];

object = [[NSObject alloc] init];
// This causes an NSInvalidArgumentException to be raised
[object thisThrowsAnException];

所以一方面,我们可以方便地不必检查 Nil(假设我们不太关心方法调用的结果)——但另一方面,我们必须检查我们的对象是否有异常不响应方法?

如果我不确定对象是否会响应,我要么必须:

@try {
    [object thisThrowsAnException];
} @catch (NSException *e){
    // do something different with object, since we can't call thisThrowsAnException
}

或者,

if([object respondsToSelector:@selector(thisThrowsAnException)]) {
    [object thisThrowsAnException];
}
else {
    // do something different with object, since we can't call thisThrowsAnException
}

(后者可能是更好的方法,因为如果 object 为 Nil,则选择器不会引发异常,因此您的代码可能不会按照您希望的方式运行)。

我的问题是:为什么 Apple 决定以这种方式实施它?
为什么不让对实例化对象的无法识别的选择器调用不引发异常?
或者,如果您尝试对其调用方法,为什么不让 Nil 对象引发异常呢?

4

3 回答 3

13

我不能完全回答你的问题,但我可以回答一部分。Objective-C 允许您向其发送消息,nil因为它使代码更加优雅。 您可以在此处阅读有关此设计决策的信息,我将窃取其示例:

假设您想获取某人在她的办公电话上拨打的最后一个电话号码。如果您无法向 发送消息nil,则必须这样编写:

Office *office = [somePerson office];
// Person might not have an office, so check it...
if (office) {
    Telephone *phone = [office telephone];
    // The office might not have a telephone, so check it...
    if (phone) {
        NSString *lastNumberDialed = [phone lastNumberDialed];
        // The phone might be brand new, so there might be no last-dialed-number...
        if (lastNumberDialed) {
            // Use the number, for example...
            [myTextField setText:lastNumberDialed];
        }
    }
}

现在假设您可以将消息发送到nil(并且总是nil返回):

NSString *lastNumberDialed = [[[somePerson office] telephone] lastNumberDialed];
if (lastNumberDialed) {
    [myTextField setText:lastNumberDialed];
}

至于为什么向对象发送无法识别的选择器会引发异常:我不确定。我怀疑这是一个错误比无害更常见。在我的代码中,我只希望在需要发送可选协议消息(例如,向委托人发送可选消息)时默默忽略无法识别的选择器。所以我希望系统将其视为错误,并在我不希望它成为错误的相对罕见的情况下让我明确表示。

请注意,您可以(在某种程度上)以几种不同的方式修补您自己的类中无法识别的选择器的处理。看一下forwardingTargetForSelector:, forwardInvocation:,doesNotRecognizeSelector:resolveInstanceMethod:的方法NSObject

于 2012-07-17T21:06:28.133 回答
5

从好的 ol' 文档中:

在 Objective-C 中,向 nil 发送消息是有效的——它在运行时根本不起作用。

至于无法识别的选择器行为的另一个问题,一个旧的 NSObject 实现文件(来自 MySTEP 库)显示罪魁祸首是 NSObject 方法-doesNotRecognizeSelector:,看起来有点如下:

- (void) doesNotRecognizeSelector:(SEL)aSelector
{
    [NSException raise:NSInvalidArgumentException
                format:@"NSObject %@[%@ %@]: selector not recognized", 
                        object_is_instance(self)?@"-":@"+",
                        NSStringFromClass([self class]), 
                        NSStringFromSelector(aSelector)];
}

这意味着可以修改 ObjC 方法,这样它们实际上就不必引发错误。这意味着该决定完全是任意的,就像将“吃方法”消息切换为 nil 的决定一样。可以通过调配 NSObject 的方法来完成的壮举(非常危险,因为它会在 mac 上引发 EXC_BAD_ACCESS 或 EXC_I386_BPT,但至少它不会引发异常)

void Swizzle(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}

-(void)example:(id)sender {
    Swizzle([NSObject class], @selector(doesNotRecognizeSelector:), @selector(description));
    [self performSelector:@selector(unrecog)];
}

类别:

@implementation NSObject (NoExceptionMessaging)

-(void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"I've got them good ol' no exception blues.");
}
@end
于 2012-07-17T21:18:04.350 回答
3

为了大家的乐趣,由于我和 CodaFi 的讨论,这里有一个快速破解的方法来吃通常没有回复的消息并让它们返回nil

@interface EaterOfBadMessages : NSObject 
@end

@implementation EaterOfBadMessages

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
{
    NSMethodSignature * sig = [super methodSignatureForSelector:aSelector];
    if( !sig ){
        sig = [NSMethodSignature signatureWithObjCTypes:"@@:"];
    }
    return sig;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation 
{
    id nilPtr = nil;
    [anInvocation setReturnValue:&nilPtr];
}

@end

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        EaterOfBadMessages * e = [[EaterOfBadMessages alloc] init];
        // Of course, pre-ARC you could write [e chewOnThis]
        NSLog(@"-[EaterOfBadMessages chewOnThis]: %@", [e performSelector:@selector(chewOnThis)]);

    }
    return 0;
}

请不要在现实生活中使用它。

于 2012-07-17T22:12:44.567 回答