Почему передача какого-либо селектора объекту Nil ничего не делает, но отправка "недопустимого" селектора в любой NSObject вызывает исключение?

Кто-нибудь знает, почему 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
}

(Последний, вероятно, лучший способ сделать это, так как если объект является Nil, селектор НЕ будет генерировать исключение, поэтому ваш код может не вести себя так, как вы хотите).

Мой вопрос: ПОЧЕМУ Apple решила реализовать его таким образом?
Почему бы не вызвать непризнанный вызов селектора, чтобы экземпляр объекта не вызывал исключение?
В качестве альтернативы, почему бы не создать объект Nil исключение, если вы попытаетесь вызвать метод на нем?

Ответ 1

Я не могу полностью ответить на ваш вопрос, но я могу ответить на его часть. 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.

Ответ 2

Из документации хорошего документа:

В Objective-C, действительно, нужно отправить сообщение в nil-it просто не имеет эффект во время выполнения.

Что касается другой проблемы непризнанного поведения селектора, старый файл реализации NSObject (из библиотеки MySTEP) показывает, что виновником является метод NSObject -doesNotRecognizeSelector:, который выглядит несколько следующим образом:

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

Это означает, что методы ObjC можно по-настоящему переделать так, чтобы на самом деле им не приходилось поднимать ошибку. Это означает, что решение было совершенно произвольным, так же как и решение переключиться на сообщения "прием пищи" до нуля. Подвиг, который можно выполнить с помощью метода swozling NSObject (совершенно опасного, поскольку он будет поднимать EXC_BAD_ACCESS или EXC_I386_BPT на Mac, но по крайней мере он не вызывает исключения)

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

Ответ 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;
}

Пожалуйста, не используйте это в реальной жизни.