Fucking Block Syntax (Objective-C)

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

Но на самом то деле, всё совсем просто 🙂

Внимание: Замыкания в Swift имеют семантику захвата, аналогичную блокам в Objective-C, но отличаются одним ключевым способом: переменные изменяются, а не копируются. Другими словами, поведение __block в Objective-C является поведением по умолчанию для переменных в Swift!

Сразу оговорюсь, что “мопед” не мой, есть ресурс с таким названием, который у меня в закладках) вот он, этот сайт не раз меня выручал. И я решил, что хватит ходить на другие ресурсы, нужно пополнять базу интересных и полезных вещей у себя.

Примеры блочного синтаксиса.

Как локальная переменная:


     returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

Как свойство:


     @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

Как параметр метода:


     - (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;

Как аргумент вызывающего метода:


     [someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

typedef


     typedef returnType (^TypeName)(parameterTypes);
     TypeName blockName = ^returnType(parameters) {...};

Больше о блоках. Вольный перевод официальной документации.

Block Syntax

Простой блок:


  ^{
      NSLog(@"This is a block");
   }

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

Вы можете использовать указатель на метод, для отслеживания блока.


     void (^simpleBlock)(void);

Если вы не привыкли иметь дело с указателями в функциях C, синтаксис может показаться немного необычным. В этом примере объявляется переменная simpleBlock для ссылки на блок, который не принимает аргументов и не возвращает значение, что означает, что переменной можно присвоить литерал блока, показанный выше, например, так:


     simpleBlock = ^{
          NSLog(@"This is a block");
     };

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


     void (^simpleBlock)(void) = ^{
        NSLog(@"This is a block");
     };

После того, как вы объявили и присвоили блочную переменную, вы можете использовать ее для вызова блока:


     simpleBlock();

Примечание. Если вы попытаетесь вызвать блок с помощью неназначенной переменной (переменная nil-блока), ваше приложение упадёт.

Блоки которые принимают аргументы и возвращают значения

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


     double (^multiplyTwoValues)(double, double);

Соответствующий литерал блока может выглядеть так:


    ^ (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

FirstValue и secondValue используются для ссылки на значения, предоставленные при вызове блока, как любое определение функции. В этом примере возвращаемый тип выводится из оператора return внутри блока. Если вы предпочитаете, вы можете сделать тип возврата явным, указав его между кареткой и списком аргументов:


    ^ double (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

После того, как вы объявили и определили блок, вы можете вызвать его так же, как функцию:


    double (^multiplyTwoValues)(double, double) = ^(double firstValue, double secondValue) {
         return firstValue * secondValue;
    };
 
    double result = multiplyTwoValues(2,4);
 
    NSLog(@"The result is %f", result);

    // The result is 8.000000

Блоки могут захватывать значения из окружающей области

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


- (void)testMethod {
     int anInteger = 42;
 
     void (^testBlock)(void) = ^{
         NSLog(@"Integer is: %i", anInteger);
     };
 
     testBlock();
}

В этом примере anInteger объявляется вне блока, но значение захватывается, когда блок был определен. Это означает, что если вы измените внешнее значение переменной между временем, в котором вы определяете блок, и временем, когда он вызывается, значение захваченное блоком не изменится. Например:


    int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    anInteger = 84;
 
    testBlock();

В результате NSLog будет отображать:


    Integer is: 42

Это также означает, что блок не может изменить значение исходной переменной или изменить захваченное значение (оно фиксируется как константа const).

Используйте модификатор __block для совместного использования переменной

Если вам необходимо изменить значение захваченной переменной из блока, вы можете использовать модификатор __block в исходном объявлении переменной. Это означает, что переменная находится в хранилище, которое совместно используется лексической областью исходной переменной и любыми блоками, объявленными в этой области.

Например, вы можете переписать предыдущий пример так:


    __block int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    anInteger = 84;
 
    testBlock();

В результате NSLog будет отображать:


    Integer is: 84

Это также означает, что блок может изменить исходное значение, например:


    __block int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
        anInteger = 100;
    };
 
    testBlock();
    NSLog(@"Value of original variable is now: %i", anInteger);

В результате NSLog будет отображать:


    Integer is: 42
    Value of original variable is now: 100

Вы можете передавать блоки в качестве аргументов для методов или функций

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

Блоки также используются для обратных вызовов (callback), определяющих код, который будет выполнен после завершения задачи. Например, вашему приложению может потребоваться отреагировать на действие пользователя, создав объект, который выполняет сложную задачу, например, запрос к серверу (наиболее распространенный случай). Поскольку задача может занять много времени, вы должны отобразить какой-то индикатор прогресса во время выполнения задачи, а затем скрыть этот индикатор, как только задача будет завершена.

Этого можно было бы достичь с помощью делегирования: вам нужно создать протокол делегирования, реализовать требуемый метод, установить свой объект в качестве делегата задачи, а затем дождаться, пока он вызовет метод делегата для вашего объекта, как только задание выполнено

Блоки делают это намного проще, однако, потому что вы можете определить поведение обратного вызова во время запуска задачи, например так:


- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];
 
    XYZWebTask *task = ...
 
    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}

В этом примере вызывается метод для отображения индикатора хода выполнения, затем создается задача и указывается, чтобы она запустилась. Блок обратного вызова определяет код, который должен быть выполнен после завершения задачи; в этом случае он просто вызывает метод, чтобы скрыть индикатор прогресса. Обратите внимание, что этот блок обратного вызова захватывает себя (self), чтобы иметь возможность вызывать метод hideProgressIndicator при завершении выполнения блока. При захвате себя (self) важно соблюдать осторожность, потому что легко создать циклическую ссылку (strong reference cycle), но об этом немного позже.

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

Объявленный для beginTaskWithCallbackBlock: метод, показанный в этом примере, будет выглядеть так:


- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;

(void (^) (void)) указывает, что параметр является блоком, который не принимает никаких аргументов и не возвращает никаких значений. Реализация метода может вызывать блок обычным способом:


- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
    ...
    callbackBlock();
}

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


- (void)doSomethingWithBlock:(void (^)(double, double))block {
    ...
    block(21.0, 2.0);
}

Блок всегда должен быть последним аргументом метода

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


- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;

Это облегчает чтение вызова метода при указании встроенного блока, например так:


    [self beginTaskWithName:@"MyTask" completion:^{
        NSLog(@"The task is complete");
    }];

Но никто не запрещает использовать в аргументах два или более блоков:


- (void)beginTaskWithName:(NSString *)name success:(void(^)(void))success failure:(void(^)(void))failure;

Теперь наш пример будет выглядеть так:


    [self beginTaskWithName:@"Task" success:^{
        NSLog(@"The task is complete with success");
    } failure:^{
        NSLog(@"The task is complete with failure");
    }];

Используйте определения типов для упрощения синтаксиса блоков

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

Например, вы можете определить тип для простого блока без аргументов или возвращаемого значения, например:


    typedef void (^XYZSimpleBlock)(void);

Затем вы можете использовать свой пользовательский тип для параметров метода или при создании блочных переменных:


    XYZSimpleBlock anotherBlock = ^{
        ...
    };


- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    ...
    callbackBlock();
}

Определения пользовательских типов особенно полезны при работе с блоками, которые возвращают блоки или принимают другие блоки в качестве аргументов. Рассмотрим следующий пример:

Осторожно в этом месте течет кровь из глаз 🙂


void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
    ...
    return ^{
        ...
    };
};

Переменная complexBlock ссылается на блок, который принимает другой блок в качестве аргумента (aBlock) и возвращает еще один блок.

Переписывание кода для использования определения типа делает этот кусок кода более читабельным:


    XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
    ...
    return ^{
        ...
    };
};

Объекты используют свойства для отслеживания блоков

Синтаксис определения свойства для отслеживания блока аналогичен переменной блока:


@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end

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

Свойство блока устанавливается или вызывается как любая другая переменная блока:


    self.blockProperty = ^{
        ...
    };
    self.blockProperty();

Также возможно использовать определения типов для объявлений свойств блока, например так:


typedef void (^XYZSimpleBlock)(void);
 
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end


Избегайте циклических ссылок при захвате себя (self)

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

Блоки поддерживают строгие ссылки на любые захваченные объекты, в том числе self, что означает, что легко получить циклическую ссылку, если, например, объект поддерживает свойство copy для блока, который захватывает self:


@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end


@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
        [self doSomething];    // capturing a strong reference to self
                               // creates a strong reference cycle
    };
}
...
@end

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

Чтобы избежать этой проблемы, рекомендуется зафиксировать слабую ссылку на себя, например:


- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];   // capture the weak reference
                                  // to avoid the reference cycle
    }
}

Захватив слабый указатель на себя, блок не будет поддерживать сильную связь с объектом XYZBlockKeeper. Если этот объект освобождается до вызова блока, указатель weakSelf будет просто установлен в nil.

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

Блоки могут упростить перечисление

В дополнение к общим обработчикам завершения (completion handlers) многие API-интерфейсы Cocoa и Cocoa Touch используют блоки для упрощения общих задач, таких как перечисление коллекций. Например, класс NSArray предлагает три метода на основе блоков, в том числе:


- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;

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


    NSArray *array = ...
    [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"Object at index %lu is %@", idx, obj);
    }];

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


    [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        if (...) {
            *stop = YES;
        }
    }];

Также можно настроить перечисление с помощью метода enumerateObjectsWithOptions: usingBlock:. Например, указание параметра NSEnumerationReverse будет выполнять итерацию по коллекции в обратном порядке.

Если код в блоке перечисления интенсивно использует процессор и безопасен для одновременного выполнения, вы можете использовать опцию NSEnumerationConcurrent:


    [array enumerateObjectsWithOptions:NSEnumerationConcurrent
                            usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        ...
    }];

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

Класс NSDictionary также предлагает блочные методы, в том числе:


    NSDictionary *dictionary = ...
    [dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
        NSLog(@"key: %@, value: %@", key, obj);
    }];

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

Использование блочных операций с очередями операций

Очередь операций – это подход Cocoa и Cocoa Touch к планированию задач. Вы создаете экземпляр NSOperation для инкапсуляции единицы работы вместе со всеми необходимыми данными, а затем добавляете эту операцию в NSOperationQueue для выполнения.

Вы можете создать свой собственный подкласс NSOperation для реализации сложных задач, также можно использовать NSBlockOperation для создания операции с использованием блока, например:


    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
    }];

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


    // schedule task on main queue:
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
    [mainQueue addOperation:operation];
 
    // schedule task on background queue:
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];

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

Блоки по расписанию в очередях Grand Central Dispatch

Если вам нужно запланировать выполнение произвольного блока кода, вы можете напрямую работать с очередями отправки, контролируемыми Grand Central Dispatch (GCD). Очереди диспетчеризации позволяют легко выполнять задачи синхронно или асинхронно по отношению к вызывающему абоненту и выполнять их задачи в порядке поступления.

Вы можете создать собственную очередь отправки или использовать одну из очередей, автоматически предоставляемых GCD. Например, если вам нужно запланировать задачу для одновременного выполнения, вы можете получить ссылку на существующую очередь, используя функцию dispatch_get_global_queue () и указав приоритет очереди, например:


     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Чтобы отправить блок в очередь, вы используете функции dispatch_async () или dispatch_sync (). Функция dispatch_async () сразу возвращается, не дожидаясь вызова блока:


    dispatch_async(queue, ^{
        NSLog(@"Block for asynchronous execution");
    });

Функция dispatch_sync () не возвращает, пока блок не завершит выполнение; вы можете использовать его в ситуации, когда параллельному блоку необходимо, например, дождаться завершения другой задачи в главном потоке, прежде чем продолжить.

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

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

Поделиться

Оставить комментарий