Как мога да създам блок, който "обвива" двойка цел/селектор?

Обичам блокове и са много готини.

Откривам обаче, че блоковете могат да претрупат кода ми и да го направят по-труден за четене, без да ги сгъвам всички в Xcode (което не ми харесва да правя).

Харесва ми да разделям кода си на логически методи (селектори), за да го поддържам по-лесен за четене, но изглежда (на повърхността), че това не е лесно възможно с рамки като dispatch, AFNetworking и няколко други.

Също така не ми пука за делегирания подход, тъй като това означава, че не мога да назова методите си така, както бих искал, вместо това разчитам на това, което други хора смятат, че имам нужда.

И така, без да пишете куп лепен код като този:

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

Вместо това бих могъл да направя нещо подобно:

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

Което е по-лесно за четене (за мен).

Със силата, която имаме с objektiv-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
@Wain едва ли. Отне ми около 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
Изглежда като ужасно много магия (но е доста готино; не разгледах подробно изпълнението, но изглежда солидно). Обърнете внимание, че третирането на сайтове за извикване без varargs като променливи аргументи при извикване е нарушение на 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

Здравейте, опитвам се да направя програма в WF, която използва събитието KeyPress. Написах следния код:

 private void Form1_KeyPress(object sender, KeyPressEventArgs e)
   {
     while (true)
     {
         switch (e.KeyChar)
         {
                    case (char)68:
                        MessageBox.Show("Test");
                        break;
         }
      }
}

Но когато изпълня програмата и натисна клавиша, полето за съобщение не се появява. Някой има ли предложения или знае ли как да поправя това? Казаха ми също, че събитие KeyDown може да работи, но и аз не знам как да работя с тях.

- 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? Може би вграден атрибут на сила ще работи. - person Richard J. Ross III; 03.05.2013
comment
@newacct Е, за бога, прав си! Просто включих показалец в него на симулатора и той работи! Оказва се, че на mac va_list вече е тип показалец зад кулисите, но не и на iOS. Благодаря, че ми помогна да разбера това! - person Richard J. Ross III; 03.05.2013