Создание динамического метода в Objective-C - PullRequest
2 голосов
/ 25 августа 2011

В книге Прагматичный программист авторы предполагают, что все входные данные метода должны быть проверены. Это позволяет рано обнаруживать проблемы с методом и легко отслеживать их источники.

В своем приложении Mac я достиг этого, создав класс Assert. Этот класс имеет несколько методов класса. Эти методы определяют, выполняется ли какое-либо предварительное условие, а если нет, то выдается исключение. Типичное утверждение может выглядеть примерно так:

-(void) setWidth: (int) theWidth {
    [Assert integer: width isGreaterThanInteger: 0];
    width = theWidth;
}

Это работает очень хорошо, и значительно сокращает время, которое я трачу на поиск ошибок. Однако в последнее время я заметил, что некоторые из методов утверждения очень полезны в качестве предикатов. Например, мои integer:isGreaterThanInteger:andLessThanInteger: и мои stringIsNotEmpty: методы одинаково полезны. С этой целью я создал второй класс Predicate, который я заполнил несколькими из моих более полезных методов предикатов. Поэтому я взял логику из методов assert и переместил ее в Predicate, а затем переписал мои Assert методы, подобные следующим:

if ![Predicate predicateMethod]
    throw exception

Это превратилось в кошмар обслуживания. Если я изменю имя метода в Predicate, я также должен изменить его в Assert, чтобы он оставался последовательным. Если я обновлю документацию по методу Assert, то я должен сделать то же самое с методом Predicate.

В идеале я хотел бы восстановить класс Assert так, чтобы при вызове любого метода он перехватывал селектор. Затем класс Predicate можно проверить, чтобы узнать, отвечает ли он на селектор, и, если это так, метод вызывается на Predicate с теми же аргументами, которые были переданы в метод Assert. Если метод Predicate возвращает false, генерируется исключение.

Есть ли способ сделать это в Objective-C?

Спасибо.

Ответы [ 3 ]

2 голосов
/ 25 августа 2011

Вы можете использовать -forwardingTargetForSelector:, чтобы просто переслать метод другому объекту, но если вам нужно расширенное поведение (например, проверка возвращаемого значения, чтобы увидеть, является ли оно ложным), вам может понадобиться использовать -forwardInvocation:. (Однако учтите, что в документации сказано, что это «намного дороже», чем предыдущий вариант.)

1 голос
/ 27 августа 2011

В итоге я переопределил resolveClassMethod:.Хотя переопределение forwardInvocation могло бы сработать (мне пришлось бы найти какой-то способ переопределить его для объекта класса), resolveClassMethod: кажется, что это более простой и эффективный метод.Вот как в конечном итоге выглядела моя окончательная реализация:

#import "Assert.h"
#import "Predicate.h"
#include <objc/objc-runtime.h>

void handlePredicateSelector(id self, SEL _cmd, ...);

@implementation Assert

+(void) failWithMessage: (NSString *) message
{
    NSLog(@"%@", message);
    [NSException raise:@"ASSERTION FAILURE" format:message];
}

+(void) fail
{
    [Assert failWithMessage:@"An unconditional failure has been detected."];
}

+(BOOL) resolveClassMethod: (SEL) selector
{
    if ([(id) [Predicate class] respondsToSelector:selector])
    {
        /*
         The meta class fix was taken from here: http://iphonedevelopment.blogspot.com/2008/08/dynamically-adding-class-objects.html
         */

        //get the method properties from the Predicate class
        Class predicateMetaClass = objc_getMetaClass([[Predicate className] UTF8String]);
        Method predicateMethod = class_getClassMethod(predicateMetaClass, selector);
        const char *encoding = method_getTypeEncoding(predicateMethod);

        Class selfMetaClass = objc_getMetaClass([[self className] UTF8String]);
        class_addMethod(selfMetaClass, selector, (IMP) handlePredicateSelector, "B@:?");

        return YES;
    }
    return [super resolveClassMethod:selector];
}

@end

void handlePredicateSelector(id self, SEL _cmd, ...)
{
    //get the number of arguments minus the self and _cmd arguments
    NSMethodSignature *predicateMethodSignature = [(id) [Predicate class] methodSignatureForSelector:_cmd];
    NSUInteger numberOfArguments = [predicateMethodSignature numberOfArguments] - 2;

    NSInvocation *predicateInvocation = [NSInvocation invocationWithMethodSignature:predicateMethodSignature];
    [predicateInvocation setTarget:[Predicate class]];
    [predicateInvocation setSelector:_cmd];

    va_list ap;
    va_start(ap, _cmd);

    for (int i = 0; i < numberOfArguments; i++)
    {
        void *arg = va_arg(ap, void *);
        [predicateInvocation setArgument:&arg atIndex:i+2];
    }

    va_end(ap);

    BOOL returnValue;
    [predicateInvocation invoke];
    [predicateInvocation getReturnValue:&returnValue];

    //determine if the assertion is true
    if (!returnValue)
    {
        [Assert failWithMessage:[NSString stringWithFormat: @"The following assertion failed: %@", NSStringFromSelector(_cmd)]];
    }
}

Единственное, что я не мог понять, это как получить кодировку типа из сигнатуры метода.Похоже, это не влияет на вывод методов, но я бы хотел исправить это, если смогу.

1 голос
/ 25 августа 2011

Если вы используете чистый Objective-C, вы должны увидеть обсуждение «Пересылка» здесь . Это в основном описывает, как сделать именно то, что вы хотите, в том числе пример кода.

Если вы используете Какао, вам, возможно, придется использовать forwardInvocation: вместо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...