Как я могу создать блок, который «обертывает» пару «цель/селектор»?

Я люблю блоки, и они очень крутые.

Тем не менее, я обнаружил, что блоки могут загромождать мой код и затруднять его чтение, не складывая их все внутри Xcode (что мне не нравится делать).

Мне нравится разбивать мой код на логические методы (селекторы), чтобы его было легче читать, но кажется (на первый взгляд), что это не всегда возможно с такими фреймворками, как диспетчеризация, AFNetworking и некоторыми другими.

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

Итак, без написания кучи связующего кода, подобного этому:

-(void) reloadData { 
    ...
    [[EventsManager instance] data:YES async:^(NSArray *events) { [self processEvents:events] }];
    ...
}

Вместо этого я мог бы сделать что-то вроде этого:

-(void) reloadData {
    ...
    [[EventsManager instance] data:YES async:createBlock(self, @selector(processEvents:))];
    ...
}

Что легче читать (мне).

С мощью, которую мы имеем с target-c, и его временем выполнения, это должно быть возможным, не так ли? Хотя ничего подобного я там не видел.


person Richard J. Ross III    schedule 27.04.2013    source источник
comment
Просто примечание для тех, кто читает это, кто может быть сбит с толку: я ответил на свой вопрос, поскольку я понял это самостоятельно.   -  person Richard J. Ross III    schedule 28.04.2013
comment
Вы справились с этим за ноль минут, молодец!   -  person Wain    schedule 28.04.2013
comment
@Вейн вряд ли. Мне потребовалось около 2 дней упорной работы, чтобы получить это.   -  person Richard J. Ross III    schedule 28.04.2013
comment
Без обид, но вы не разместили это как вопрос, поэтому, вероятно, это должна быть вики сообщества?   -  person Wain    schedule 28.04.2013
comment
@wain нет, такие вопросы с самостоятельными ответами рекомендуется не использовать в CW: meta.stackexchange.com/questions/153113/   -  person Richard J. Ross III    schedule 28.04.2013
comment
@Wain Вопросы с самостоятельными ответами разрешены и активно поощряются, если они соответствуют обычным правилам и стандартам для ответов и вопросов. Здесь нет никаких проблем и нет необходимости делать что-либо вики сообщества. Любая репутация, полученная благодаря голосованию, заслужена.   -  person Bart    schedule 29.04.2013
comment
Я знаю, что немного опоздал на вечеринку, но разве мы не изобрели батуты, чтобы решить эту проблему? Конечно, ребятам из GitHub нравится возиться с препроцессором больше, чем со средой выполнения, но все же.   -  person CodaFi    schedule 24.06.2013
comment
@CodaFi Насколько я могу судить, вы передаете этому методу блок, и он вызывает его динамически. Вместо этого создается блок из пары "цель-селектор".   -  person Richard J. Ross III    schedule 24.06.2013
comment
Но вы можете видеть, как легко было бы преобразовать батут в это. Пусть батут вернет блок, созданный с помощью метамакросов в этом файле.   -  person CodaFi    schedule 24.06.2013
comment
Селекторы @CodaFi немного сложнее, чем блок, особенно с типами, которые не являются объектами, что чаще встречается в методах, чем в блоках. Кроме того, C++ также решает эту проблему, шаблоны могут делать все это во время компиляции без UB.   -  person Richard J. Ross III    schedule 24.06.2013


Ответы (2)


Мне понравился ваш ответ с академической точки зрения; +1 и, очевидно, вы чему-то научились.

С практической точки зрения кажется, что очень много добавленной уязвимости для очень небольшого сокращения набора текста, в то время как это также приводит к некоторой потере информации в месте вызова.

Преимущество этого в том, что это точно явно:

-(void) reloadData { 
    ...
    [[EventsManager instance] data:YES async:^(NSArray *events) { [self processEvents:events] }];
    ...
}

Читая это, можно увидеть, что асинхронный блок обратного вызова требуется для обработки аргументов и что метод processEvents: для self будет использоваться для выполнения фактической работы.

Выражение createBlock(self, @selector(processEvents:)) является его представлением с потерями; он теряет явную аргументацию обратного вызова и сопоставление между этой аргументацией и вызываемым методом (я часто вижу блоки обратного вызова, подобные приведенным выше, с несколькими аргументами, где есть некоторая упрощенная логика и/или обработка аргументов перед вызовом метода).

Также обратите внимание, что обработка сайта вызова без varargs как varargs при вызове является нарушением стандарта C и не будет работать с некоторыми ABI с определенными списками аргументов.

person bbum    schedule 27.04.2013
comment
@RichardJ.RossIII - я не знаю, нужно ли нам быть здесь такими строгими. Он добавил некоторую полезную информацию к вашему вопросу, на который вы сами ответили, поэтому я не думаю, что это квалифицируется как разбитое окно, которое нужно удалить. Его ответ не будет работать в комментарии, поэтому у меня нет проблем с его сохранением. - person Brad Larson; 28.04.2013

Да, это действительно возможно, но это решение специфично для ABI (работа на всех платформах не гарантируется) и широко использует доступную во время выполнения информацию о методах.

Сначала нам нужно получить информацию о методе, который мы оборачиваем блоком. Это делается через NSMethodSignature, который содержит такую ​​информацию, как:

  • Количество аргументов
  • Размер (в байтах) каждого аргумента
  • Размер возвращаемого типа

Это позволяет нам обернуть (почти) любой метод без специального кода для этого метода, создавая таким образом повторно используемую функцию.

Во-вторых, нам нужен способ безопасной отправки вызовов методов во время выполнения. Мы делаем это с помощью NSInvocation, что дает нам возможность создать динамический и безопасный вызов метода во время выполнения.

В-третьих, нам нужен блок, который может принимать любое количество передаваемых аргументов, и диспетчеризация этого. Это делается с помощью va_list API C и должно работать для 99% методов.

Наконец, нам нужно получить возвращаемое значение и иметь возможность вернуть его из нашего блока. Это часть всей операции, которая может не работать из-за странностей с возвращаемыми структурами и тому подобным во время выполнения Objective-C.

Однако, пока вы придерживаетесь примитивных типов и объектов Objective-C, этот код должен отлично работать для вас.

Несколько замечаний по поводу этой реализации:

  • Это зависит от неопределенного поведения с приведением типов блоков и функций, однако из-за соглашений о вызовах iOS и Mac это не должно вызывать никаких проблем (если только ваш метод не имеет тип возвращаемого значения, отличный от ожидаемого блоком).

  • Это также зависит от неопределенного поведения с результатом вызова va_arg с типом, который может не совпадать с переданным, однако, поскольку типы имеют одинаковый размер, это никогда не должно быть проблемой.

Без лишних слов, вот пример кода, за которым следует реализация:


@interface MyObj : NSObject

-(void) doSomething;

@end

@implementation MyObj

-(void) doSomething
{
    NSLog(@"This is me, doing something! %p", self);
}

-(id) doSomethingWithArgs:(long) arg :(short) arg2{
    return [NSString stringWithFormat:@"%ld %d", arg, arg2];
}

@end

int main() {
    // try out our selector wrapping
    MyObj *obj = [MyObj new];

    id (^asBlock)(long, short) = createBlock(obj, @selector(doSomethingWithArgs::));
    NSLog(@"%@", asBlock(123456789, 456));
}

/* WARNING, ABI SPECIFIC, BLAH BLAH BLAH NOT PORTABLE! */
static inline void getArgFromListOfSize(va_list *args, void *first, size_t size, size_t align, void *dst, BOOL isFirst) {
    // create a map of sizes to types
    switch (size) {
            // varargs are weird, and are aligned to 32 bit boundaries. We still only copy the size needed, though.
            // these cases should cover all 32 bit pointers (iOS), boolean values, and floats too.
        case sizeof(uint8_t): {
            uint8_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

        case sizeof(uint16_t): {
            uint16_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

        case sizeof(uint32_t): {
            uint32_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

            // this should cover 64 bit pointers (Mac), and longs, and doubles
        case sizeof(uint64_t): {
            uint64_t tmp = isFirst ? (uint64_t) first : va_arg(*args, uint64_t);
            memcpy(dst, &tmp, size);
            break;
        }
            /* This has to be commented out to work on iOS (as CGSizes are 64 bits)
            // common 'other' types (covers CGSize, CGPoint)
        case sizeof(CGPoint): {
            CGPoint tmp = isFirst ? *(CGPoint *) &first : va_arg(*args, CGPoint);
            memcpy(dst, &tmp, size);
            break;
        }
             */

            // CGRects are fairly common on iOS, so we'll include those as well
        case sizeof(CGRect): {
            CGRect tmp = isFirst ? *(CGRect *) &first : va_arg(*args, CGRect);
            memcpy(dst, &tmp, size);
            break;
        }

        default: {
            fprintf(stderr, "WARNING! Could not bind parameter of size %zu, unkown type! Going to have problems down the road!", size);
            break;
        }
    }
}

id createBlock(id self, SEL _cmd) {
    NSMethodSignature *methodSig = [self methodSignatureForSelector:_cmd];

    if (methodSig == nil)
        return nil;

    return ^(void *arg, ...) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];

        [invocation setTarget:self];
        [invocation setSelector:_cmd];

        NSUInteger argc = [methodSig numberOfArguments];
        va_list args;
        va_start(args, arg);

        for (int argi = 2; argi < argc; argi++) {
            const char *type = [methodSig getArgumentTypeAtIndex:argi];

            NSUInteger size;
            NSUInteger align;

            // get the size
            NSGetSizeAndAlignment(type, &size, &align);

            // find the right type
            void *argument = alloca(size);

            getArgFromListOfSize(&args, arg, size, align, argument, argi == 2);

            [invocation setArgument:argument atIndex:argi];
        }

        va_end(args);

        [invocation invoke];

        // get the return value
        if (methodSig.methodReturnLength != 0) {
            void *retVal = alloca(methodSig.methodReturnLength);
            [invocation getReturnValue:retVal];

            return *((void **) retVal);
        }

        return nil;
    };
}

Дайте мне знать, если у вас возникнут проблемы с этой реализацией!

person Richard J. Ross III    schedule 27.04.2013
comment
Кажется, что это ужасно много волшебства (но это довольно круто; я не смотрел на реализацию в деталях, но она выглядит солидно). Обратите внимание, что обработка сайтов вызова без переменных аргументов как переменных аргументов при вызове является нарушением C ABI и будет прерываться на определенных архитектурах с определенными списками аргументов. - person bbum; 28.04.2013
comment
@bbum это правильно, и я включил это в свой ответ, это на арках, где наиболее вероятно будет использоваться objc (x86, ARM), это не проблема. Фактически, я использовал то же самое в своем известном приложении для iOS в C-ответе. - person Richard J. Ross III; 28.04.2013
comment
первый аргумент не должен быть объявлен id, так как ARC попытается сохранить его, и если это не id, произойдет сбой - person newacct; 28.04.2013
comment
@newacct оказывается, что проблема была в порядке следования байтов, теперь это должно быть исправлено, попробуйте сейчас! - person Richard J. Ross III; 28.04.2013
comment
У меня другая проблема (симулятор iOS): @implementation MyObj -(id) doSomethingWithArgs:(char)arg :(char)arg2 :(char)arg3 { return [NSString stringWithFormat:@"%d %d %d", arg, arg2, arg3]; } @end ... а потом выдает неверный результат: MyObj *obj = [MyObj new]; NSLog(@"%@", [obj doSomethingWithArgs:42 :17 :5]); /* prints 42 17 5 */ id (^asBlock)(char, char, char) = createBlock(obj, @selector(doSomethingWithArgs:::)); NSLog(@"%@", asBlock(42, 17, 5)); /* prints 42 17 17 */ - person newacct; 01.05.2013
comment
@newacct интересно. Вполне возможно, что это результат неопределенного поведения, о котором говорит bbum (поскольку целые числа с функциями varargs выравниваются по 32-битным границам, а не по 8-битным), но позвольте мне посмотреть, что я могу сделать. - person Richard J. Ross III; 01.05.2013
comment
@newacct Я не могу воспроизвести (запустив его на Mac OSX). Вы уверены, что у вас актуальная версия реализации? - person Richard J. Ross III; 01.05.2013
comment
@RichardJ.RossIII: у меня это происходит в симуляторе iOS, а не в приложении для Mac. попробую отладить дальше - person newacct; 02.05.2013
comment
@RichardJ.RossIII: при дальнейшей отладке это, по-видимому, вызвано передачей va_list функции getArgFromListOfSize. Стандарт C99, раздел 7.15 пар. 3: Объект ap может быть передан в качестве аргумента другой функции; если эта функция вызывает макрос va_arg с параметром ap, значение ap в вызывающей функции является неопределенным и должно быть передано в макрос va_end до любой дальнейшей ссылки на ap. Таким образом, кажется, что вы не можете многократно передавать va_list другой функции, чтобы она вызывала va_arg. Сноска в стандарте предлагает вместо этого передать va_list * - person newacct; 03.05.2013
comment
@newacct Очень интересно. Возможно ли, что он встроен в Mac, но не в iPhone? Возможно, сработает встроенный атрибут force. - person Richard J. Ross III; 03.05.2013
comment
@newacct Ну, черт возьми, ты прав! Я просто воткнул в него указатель на симуляторе и он работает! Оказывается, на Mac va_list уже является типом указателя за кулисами, но не на iOS. Спасибо, что помогли мне разобраться! - person Richard J. Ross III; 03.05.2013