9

Uwielbiam bloki, a one są bardzo fajne.Jak utworzyć blok, który "opakowuje" parę docelową/selektorową?

Uważam jednak, że bloki mogą zagracać mój kod i utrudniają czytanie bez złożenia wszystkich w Xcode (co nie podoba mi się).

Lubię dzielić mój kod na logiczne metody (selektory), aby łatwiej było je czytać, ale wydaje się (na powierzchni), że nie jest to możliwe z takimi strukturami jak wysyłanie, AFNetworking i kilka innych.

Ja również nie dbam o podejście delegatów, ponieważ oznacza to, że nie mogę nazwać swoich metod tak, jak bym tego chciała, zamiast polegać na tym, co inni ludzie uważają za potrzebną.

Tak, bez konieczności pisania kodu kilka kleju tak:

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

mogłem zamiast zrobić coś takiego:

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

Który jest łatwiejszy do odczytu (do mnie).

Z mocą, którą mamy z obiektywem c, a jest to środowisko wykonawcze, to powinno być możliwe, nie? Jednak nie widziałem czegoś podobnego.

+0

Tylko uwaga dla każdego, kto czyta to, co może być zdezorientowane: Odpowiedziałem na własne pytanie, ponieważ sam to wymyśliłem. –

+1

Wypracowałeś to w zero minut na płasko, dobra robota! – Wain

+0

@Nie prawie. Zabrało mi to około 2 dni solidnej pracy. –

Odpowiedz

6

Tak, jest to rzeczywiście możliwe, ale to rozwiązanie jest specyficzne dla ABI (nie gwarantuje się działania na wszystkich platformach) i szeroko wykorzystuje informacje dostępne w czasie wykonywania na temat metod.

Najpierw musimy uzyskać informacje o metodzie, którą pakujemy w bloku. Odbywa się to poprzez NSMethodSignature, który zawiera informacje takie jak:

  • liczba argumentów
  • rozmiar (w bajtach) każdego argumentu
  • Rozmiar typ zwracany

To pozwala nam owinąć (prawie) dowolna metoda bez określonego kodu dla tej metody, tworząc w ten sposób funkcję wielokrotnego użytku.

Po drugie, potrzebujemy sposobu na bezpieczne wysyłanie wywołań metod w czasie wykonywania. Robimy to poprzez NSInvocation, co daje nam możliwość tworzenia dynamicznego i bezpiecznego wywołania metody w czasie wykonywania.

Po trzecie, musimy mieć blok, który może przyjąć dowolną liczbę argumentów, a także wysłanie tego. Odbywa się to przez interfejsy API C va_list i powinno działać na 99% metod.

Wreszcie, musimy uzyskać wartość zwracaną i móc ją zwrócić z naszego bloku. Jest to część całej operacji, która może nie działać, z powodu dziwności z powracającymi strukturami i innymi w środowisku wykonawczym Objective-C.

Jednak dopóki zachowacie prymitywne typy i obiekty Objective-C, ten kod powinien zadziałać doskonale.

Kilka rzeczy, aby pamiętać o tym realizacji:

  • Jest to uzależnione od niezdefiniowanej zachowań z odlewania bloku & typów funkcyjnych, jednak ze względu na konwencje powołaniu iOS i Mac, nie powinno stwarzają wszelkie problemy (chyba że twoja metoda ma inny typ zwrotu niż ten, którego oczekuje blok).

  • Podlega również niezdefiniowanemu zachowaniu z wynikiem wywołania va_arg z typem, który może nie być tym, co jest przekazywane - jednak, ponieważ typy mają ten sam rozmiar, nigdy nie powinno to stanowić problemu.

Bez zbędnych ceregieli, tutaj jest przykład kodu, a następnie realizacji:


@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; 
    }; 
} 

Daj mi znać, jeśli masz jakiekolwiek problemy z ta implementacja!

+1

Wydaje się, że to strasznie dużo magii (ale jest całkiem fajne, nie patrzyłem szczegółowo na implementację, ale wygląda na solidną). Zauważ, że traktowanie stron wywołujących nie-varargi jako argumentów zmiennych, gdy jest wywoływane, jest naruszeniem ABI ABI i * spowoduje złamanie * pewnych architektur z pewnymi listami argumentów. – bbum

+0

@bbum, który jest poprawny, i zawarłem to w mojej odpowiedzi, to na archs, gdzie najczęściej jest używane obstawianie (x86, ARM) to nie jest problem. W rzeczywistości użyłem tego samego w mojej słynnej odpowiedzi "iOS app in C". –

+0

pierwszy argument nie powinien być zadeklarowany jako "id", ponieważ ARC spróbuje go zachować, a jeśli nie jest "id", spowoduje awarię – newacct

14

Podobała mi się twoja odpowiedź z akademickiego punktu widzenia; +1 i, oczywiście, nauczyłeś się czegoś.

Z praktycznego punktu widzenia, wydaje się, że jest bardzo dużo kruchości przy niewielkiej redukcji pisania, a także prowadzi do utraty informacji na stronie połączenia.

Zaletą tego rozwiązania jest to, że jest dokładnie wyraźne:

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

Czytając, że jeden widzi, że asynchroniczny blok zwrotna musi przetworzyć argumenty i że metoda processEvents: na self zostaną wykorzystane do zrobienia rzeczywista praca.

Wyrażenie createBlock(self, @selector(processEvents:)) jest stratną reprezentacją tego samego; traci on jawną argumentację wywołania zwrotnego i odwzorowanie między tą argumentacją a wywołaną metodą (często widzę bloki zwrotne, jak powyższe, z wieloma argumentami, w których istnieje lekka logika i/lub przetwarzanie argumentów przed wywołaniem metody).

Należy również zauważyć, że przetwarzanie strony wywołania innej niż varargs jako varargs, gdy jest wywoływana, stanowi naruszenie standardu C i nie działa w przypadku niektórych ABI z pewną listą argumentów.

+8

@ RichardJ.RossIII - Nie wiem, że musimy być tu tak surowi. Dodał kilka dobrych informacji do twojego pytania, które można zadać samemu sobie, więc nie sądzę, żeby to zostało usunięte jako uszkodzone okno do usunięcia. Jego odpowiedź nie zadziałałaby w komentarzu, więc nie mam z tym żadnych problemów. –

Powiązane problemy