О блоках и их использовании в Objective-C часть 2-ая

    Продолжение топика — О блоках и их использовании в Objective-C часть 1-ая.

    Многие из тех кто впервые сталкивается с блоками (или замыканиями), задают вопрос — «а зачем? Если и без них можно». Да можно. Но использование блоков имеет не мало преимуществ, и первое из них — существенная экономия на количестве кода, а следовательно и на времени написания и поддержке. Дальше буду говорить примерами.

    Содержание:


    1. Работа с контейнерами на примере NSArray.
    2. Guards на примере UITableView.
    3. Использование блоков вместо классов на примере scheduled операций.
    4. Блоки вместо делегатов в UIAlertView.
    5. UIView анимации, последовательность анимаций.
    6. Асинхронные операции и управление ими. Переписываем пример с анимациями.

    1. Работа с контейнерами на примере NSArray.

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

    Задача1

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

    Решение1:
    NSArray* stringsLengths( NSArray* strings_ )
    {
      NSMutableArray* strings_lengths_ =
       [ NSMutableArray arrayWithCapacity: [ strings_ count ] ];
      for ( NSString* string_ in strings_ )
      {
       NSNumber* length_ = [ NSNumber numberWithUnsignedInt: [ string_ length ] ];
       [ strings_lengths_ addObject: length_ ];
      }
      return [ NSArray arrayWithArray: strings_lengths_ ];
    }


    * This source code was highlighted with Source Code Highlighter.

    Решение2 с блоками:
    NSArray* stringsLengths( NSArray* strings_ )
    {
      return [ strings_ map: ^( id string_ )
      {
       return (id)[ NSNumber numberWithUnsignedInt: [ string_ length ] ];
      } ];
    }


    * This source code was highlighted with Source Code Highlighter.

    Задача2

    Задан массив структур с таким интерфейсом:
    @interface Element : NSObject

    @property ( nonatomic, retain, readonly ) NSArray* subElements;

    @end


    * This source code was highlighted with Source Code Highlighter.
    нужно создать новый массив, который содержит все элементы всех subElements.

    Решение1:
    NSArray* allSubElements( NSArray* elements_ )
    {
      NSMutableArray* result_ = [ NSMutableArray array ];

      for ( Element* element_ in elements_ )
      {
       NSArray* object_items_ = element_.subElements;
       [ result_ addObjectsFromArray: object_items_ ];
      };

      return [ NSArray arrayWithArray: result_ ];
    }


    * This source code was highlighted with Source Code Highlighter.

    Решение2 с блоками:
    NSArray* allSubElements( NSArray* elements_ )
    {
      return [ elements_ flatten: ^( id element_ )
      {
       return [ element_ subElements ];
      } ];
    }


    * This source code was highlighted with Source Code Highlighter.

    Еще несколько удобных расширений класса NSArray можно найти в файле NSArray+BlocksAdditions.m

    2. Guards на примере UITableView.

    Если необходимо выполнить несколько подряд апдейтов контента в UITableView с анимациями (например один элемент добавить, другой удалить), то что бы в результирующей анимации все выглядело аккуратно, необходимо эти действия поместить в пределах вызова двух методов: beginUpdates и endUpdates. В таком коде можно допустить несколько ошибок, например такую:
    [ self beginUpdates ];

    [ self.tableView deleteRowsAtIndexPaths: delete_index_pathes_
                withRowAnimation: UITableViewRowAnimationBottom ];

    //здесь ошибка если condition_ == true, мы не вызовем endUpdates
    if ( condition_ )
      return;

    [ self.tableView insertRowsAtIndexPaths: insert_index_pathes_
                withRowAnimation: UITableViewRowAnimationTop ];

    [ self endUpdates ];


    * This source code was highlighted with Source Code Highlighter.

    В таких случаях, как и вслучае работы с файлами или другими ресурсами которые нужно освобождать, на помощь приходят так называемые охранники-guards, которые легко реализуются с помощью блоков. Добавим расширение класса UITableView с методом withinUpdates:
    @interface UITableView (BlocksAdditions)

    -(void)withinUpdates:( void (^)( void ) )block_;

    @end

    @implementation UITableView (BlocksAdditions)

    -(void)withinUpdates:( void (^)( void ) )block_
    {
      [ self beginUpdates ];

      @try
      {
       block_();
      }
      @finally
      {
       [ self endUpdates ];
      }
    }

    @end


    * This source code was highlighted with Source Code Highlighter.

    исправляем ошибку с анимациями:
    [ self.tableView withinUpdates: ^( void )
    {
      [ self.tableView deleteRowsAtIndexPaths: delete_index_pathes_
                 withRowAnimation: UITableViewRowAnimationBottom ];

      if ( condition_ )
       return;

      [ self.tableView insertRowsAtIndexPaths: insert_index_pathes_
                 withRowAnimation: UITableViewRowAnimationTop ];
    } ];


    * This source code was highlighted with Source Code Highlighter.

    3. Использование блоков вместо классов на примере scheduled операций.

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

    Те кто тесно работал с методами: – [ NSObject performSelector:withObject:afterDelay: ] и + [ NSTimer timerWithTimeInterval:target:selector:userInfo:repeats: ] полагаю заметили что для «target» будет вызван метод «retain» в момент создания отложеного вызова, и «release», если запланированые действия больше не будут вызыватся. Как показывает практика, такое поведение не очень удобно, так как часто требует написания дополнительной логики по вызову + [ NSObject cancelPreviousPerformRequestsWithTarget: ] и — [ NSTimer invalidate ] для отмены запланированых вызовов, и последующей возможности освобождения объекта «target».
    Здесь мы приходим к идее того что, было бы удобней иметь возможность создать отложеный вызов, который бы не вызывал «retain» и самоотменялся при удалении «target» из памяти.
    Нашей целью будет написание метода работающего описаным выше способом с таким интерфейсом:
    @interface NSObject (Scheduler)

    -(void)performSelector:( SEL )selector_
             timeInterval:( NSTimeInterval )time_interval_
                userInfo:( id )user_info_
                repeats:( BOOL )repeats_;

    @end

    * This source code was highlighted with Source Code Highlighter.

    Для начала реализуем класс JFFScheduler с таким интерфейсом:
    //тип блока для отмены запланированого действия
    typedef void (^JFFCancelScheduledBlock) ( void );
    //тип блока сожержащего запланированое действие
    typedef void (^JFFScheduledBlock) ( JFFCancelScheduledBlock cancel_ );

    @interface JFFScheduler : NSObject

    //создать новый "планировщик"
    +(id)scheduler;

    //получить "общий планировщик"
    +(id)sharedScheduler;

    //добавить новое запланированое действие
    //результат - блок для отмены запланированого действия
    -(JFFCancelScheduledBlock)addBlock:( JFFScheduledBlock )block_
                 duration:( NSTimeInterval )duration_;

    //отмена всех запланированых действий для скедулера, вызывается также в dealloc класса JFFScheduler
    -(void)cancelAllScheduledOperations;

    @end


    * This source code was highlighted with Source Code Highlighter.

    Реализация метода -(JFFCancelScheduledBlock)addBlock:( JFFScheduledBlock )block_ duration:( NSTimeInterval )duration_
    -(JFFCancelScheduledBlock)addBlock:( JFFScheduledBlock )block_
                 duration:( NSTimeInterval )duration_
    {
      //просто хранящий блок класс
      JFFSimpleBlockHolder* cancel_block_holder_ = [ JFFSimpleBlockHolder simpleBlockHolder ];

      block_ = [ [ block_ copy ] autorelease ];
      //заворачиаем объект block_ в блок без аргументов
      //что бы можно было вызвать его методом performBlock
      void (^schedule_block_) ( void ) = [ [ ^
      {
       block_( cancel_block_holder_.simpleBlock );
      } copy ] autorelease ];

      //запускаем таймер с блоком вместо "target"
      __block NSTimer* timer_ = [ NSTimer scheduledTimerWithTimeInterval: duration_
                                    target: schedule_block_
                                   selector: @selector( performBlock )
                                   userInfo: nil
                                   repeats: YES ];

      __block NSObject* cancel_ptr_ = nil;
      __block JFFScheduler* scheduler_ = self;

      //создам блок для отмены запланированого вызова
      cancel_block_holder_.simpleBlock = ^
      {
       if ( scheduler_ )
       {
         [ timer_ invalidate ];
         //удаляем выполненный блок
         [ scheduler_.cancelBlocks removeObject: cancel_ptr_ ];
         scheduler_ = nil;
       }
      };

      cancel_ptr_ = (id)cancel_block_holder_.simpleBlock;
      //сохраняем блок отмены для запланированого вызова в dealloc
      [ self.cancelBlocks addObject: cancel_ptr_ ];

      return cancel_block_holder_.simpleBlock;
    }


    * This source code was highlighted with Source Code Highlighter.

    Весь код релизации класса JFFScheduler.

    Дальше что бы все работало, нам понадобится несколько дополнительных методов:
    1. — [ NSObject performBlock ] — выполнить блок
    @implementation NSObject (PerformBlock)

    //вызвать себя как блок
    -(void)performBlock
    {
      void* self_ = self;
      JFFSimpleBlock block_ = (JFFSimpleBlock)self_;
      block_();
    }

    @end

    //пример использования
    [ ^ {
      NSLog( @"test" );
    } performBlock ];


    * This source code was highlighted with Source Code Highlighter.

    2. — [ NSString numberOfCharacterFromString: ] количество вхождений символа заданого в строке. Пример:
    NSLog( @"num of \":\" - %d", [ @":test:" numberOfCharacterFromString: @":" ] );

    * This source code was highlighted with Source Code Highlighter.

    печатает — num of ":" — 2

    3. — [ NSObject addOnDeallocBlock: ] — добавить блок, который должен выполнится при удалении владельца из памяти (в методе dealloc). Пример:
    NSObject* object_ = [ [ NSObject alloc ] init ];
    [ object_ addOnDeallocBlock: ^
    {
      NSLog( @"test" );
    } ];
    //здеь печатается - test
    [ object_ release ];


    * This source code was highlighted with Source Code Highlighter.

    Теперь у нас есть все необходимое для реализации первичной задачи главы — написания метода порождающего отложеный вызов после заданого времени, который не вызывает «retain» для «target». Реализация:
    -(void)performSelector:( SEL )selector_
       timeInterval:( NSTimeInterval )time_interval_
        userInfo:( id )user_info_
        repeats:( BOOL )repeats_
    {
     //валидация аргументов
     NSString* selector_string_ = NSStringFromSelector( selector_ );
     NSUInteger num_of_args_ = [ selector_string_ numberOfCharacterFromString: @":" ];
     NSString* assert_warning_ = [ NSString stringWithFormat: @"selector \"%@\" should has 0 or 1 parameters", selector_string_ ];
     NSAssert( num_of_args_ == 0 || num_of_args_ == 1, assert_warning_ );

     //что бы избежать циклической ссылки - исользуем в блоке __block self_ вместо self
     __block id self_ = self;

     //создаем scheduled блок, который вызывает селектор
     JFFScheduledBlock block_ = ^( JFFCancelScheduledBlock cancel_ )
     {
      //отменяем дальнейшее выполнение блока если повторы не нужны
      if ( !repeats_ )
      {
       [ self_ removeOnDeallocBlock: cancel_ ];
       cancel_();
      }

      //вызываем селектор
      num_of_args_ == 1
       ? objc_msgSend( self_, selector_, user_info_ )
       : objc_msgSend( self_, selector_ );
     };

     JFFScheduler* scheduler_ = [ JFFScheduler sharedScheduler ];

     //планируем вызовы блока с заданным интервалом
     JFFCancelScheduledBlock cancel_ = [ scheduler_ addBlock: block_
                        duration: time_interval_ ];
     //отменяем выполнение scheduled блока при удалнии из памяти объекта self
     [ self addOnDeallocBlock: cancel_ ];
    }


    * This source code was highlighted with Source Code Highlighter.

    И конечно же пример использования этого метода:
    SomeClass* object_ = [ [ SomeClass alloc ] init ];

    //создать отложеный вызов метода print
    [ object_ performSelector: @selector( print )
           timeInterval: 1.
             userInfo: nil
             repeats: NO ];

    //после release метод print не вызовется никогда,
    //так как таймер отменяется при удалении object_ из памяти
    [ object_ release ];

    * This source code was highlighted with Source Code Highlighter.

    Итог. Подобный метод в проекте который я пишу существовал и до появления блоков, но его имплементация содержала ровно в 2.5-ой раза больше строк кода и исправлялись не один раз. Реализация же с блоками далась мне с первого раза и до сих пор ошибок не находили. Надеюсь приведеные примеры были для Вас интересны.

    Продолжение: «О блоках и их использовании в Objective-C часть 3-ая»
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      +2
      Спасибо за статью, очень полезная информация.
        0
        Спасибо — позновательно
          +2
          Да, после C# или Python конечно лямбды выглядят сильно мудреными. Если в C# для функции обратного вызова я буду писать лямбду, то в Objective-C я пока лучше по старинке с селектором, чтобы не мудрить с памятью. Хотя может дело привычки.

          В любом случае хотя бы map в массиве уже отличный плюс. Джавистам о таком только мечтать)
            0
            Исправьте слово «Сожержание» в начале статьи
              0
              Поправил, спасибо
              +2
              Мерзковатое оформления кода.
                0
                Может что посоветуешь получше чем Source Code Highlighter, был бы очень благодарен.
                  0
                  </source lang="cpp"><//source> попробуйте, хотя не уверен что будет выглядеть лучше.
                0
                Чего-то я не очень понял преимущество блоков, ибо первый же пример не рабочий…

                NSArray* stringsLengths( NSArray* strings_ )
                {
                 return [ strings_ map: ^( id string_ )
                 {
                  return (id)[ NSNumber numberWithUnsignedInt: [ string_ length ] ];
                 } ];
                }


                * This source code was highlighted with Source Code Highlighter.

                warning: 'NSArray' may not respond to '-map:'

                  +1
                  я кажется понял — вы рекомендуете подключить внешнюю библиотеку, чтобы код работал. Ну тогда очевидно что сравнение не корректно, ибо решение 1 не использузет никаких внешних библиотек, а решение 2 использует. Сделать решение 1 более «элегантно» но чтобы оно использовало внешнюю библиотеку (без блоков) вполне себе возмоножно.
                  Т.е. аргумент
                  Но использование блоков имеет не мало преимуществ, и первое из них — существенная экономия на количестве кода,
                  не состоятелен (код был вынесен во внешнюю библиотеку). Впрочем продолжение
                  а следовательно и на времени написания и поддержке.
                  Очень сомнительно — ибо лучше 10 строк чистого для понимания кода против одной строчки в которой черт ногу сломит…
                  Топик плюсанул ибо он наконец мне помог понять мне что блоки мне совсем не нужны.
                    0
                    Поддерживаю жуткую нечитаемость кода. Хотя, возможно, это дело привычки.
                      0
                      аргумент: «существенная экономия на количестве кода» — не состоятелен (код был вынесен во внешнюю библиотеку).

                      Метод map: пишется один раз, использовать можно множество раз, тут и экономия. В моем проекте map: встречается 44 раза
                      Очень сомнительно — ибо лучше 10 строк чистого для понимания кода против одной строчки в которой черт ногу сломит…

                      Имя метода map: выбрано не случайно, метод map: с аналогичным поведением встечается фактически во всех языках программирования и многим, включая меня проще читать map: вместо 10 строчек кода.
                      Топик плюсанул ибо он наконец мне помог понять мне что блоки мне совсем не нужны.

                      Полагаю что с блоками придется ознакомится всем IOS разработчикам ибо многое новое апи IOS SDK идет c использованием блоков, без альтернативы с селекторами. Пример
                        0
                        > Метод map: пишется один раз
                        Ну так и без блоков этот метод написать один раз легко — что мешает использовать не блок а указатель на функцию коли хочется параметром код передавать?

                        одно дело использовать по необходимости когда альтернатив нет, другое дело сознательно. Я тоже как ни странно делал код с блоками — надо было проверить не расширит ли возможности такой вариант работы с акселерометром. Не расширил. А вот код стал более громоздким — это точно. Хотя конечно — на вкус и цвет все фломастеры разные :)
                        0
                        Топик плюсанул ибо он наконец мне помог понять мне что блоки мне совсем не нужны.

                        Вам следует срочно прочитать уже успевшие стать «старой классикой» мануалы «Grand Central Dispatch (GCD) Reference» и «Concurrency Programming Guide». А то вдруг где в разговоре всплывает, как сейчас — люди не поймут же. Это по сути сейчас основы и писать код без использования подобных бустов — это, простите, но писать говнокод.
                          0
                          не прощу. Мануалы, что вы порекомендовали — относятся к iOS4 и выше — т.е. весь код что написан до iOS 4 — говнокод? ну-ну.
                            0
                            Лично мне было очень трудно без блоков в IOS ver. < 4.0, потому написал даже рантайм
                              0
                              Нет конечно, уверен что хороший код, но продолжать отказываться от них сейчас — это действительно неблагоразумно.

                              Кстати на самом деле блоки в мак тусовке активно муссировались задолго до официального выхода (10.6 и iOS 4), Apple сама их начала пытаться продвинуть как стандарт еще в 2008 году, а поскольку все сурсы открыты, то почти сразу же был отпочкован проект plblocks который позволял насладится ими и в леопарде, ну и в первых версиях iOS (не уверен, ибо не пишу под iOS, но вроде как еще с 2 версии).

                              Ну а после уже официального представления прошел же целый год, за это время блоки и gcd действительно стали неотъемлемой частью практически любой программы. Если спросить меня что самое запоминающееся появилось у Apple за последние несколько лет, я назову именно gcd и блоки.
                            0
                            Немного с вами не согласен. Блоки, возможно, и не панацея, однако при работе с теми же интерфейсами зачастую просто не нравится определять половину логики в одном методе, половину в другом.
                            Я имею ввиду, например, UIAlertView — ради зачастую пары строк, обрабатывающих действия по выбору пользователя, приходилось заводить новый метод + еще и определять, от какого из UIAlertView (если их несколько) мы пришли…
                          +2
                          хотелось бы еще добавить, что при объявлении и реализации блока очень помогает набрать inlineBlock и получить такой хинт:
                          <#Return Type#>(^<#Block Name#>)(<#Parameter Types#>) = ^(<#Parameters#>) {
                          <#Code#>
                          };
                            0
                            Насчет блоков для UIAlertView есть хорошая надстройка от Mugunth Kumar — MKBlockAdditions
                              0
                              Еще бы, не один же я блоки люблю использовать :).

                            Only users with full accounts can post comments. Log in, please.