Работа с блоками

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

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

Эта глава объясняет синтаксис, чтобы объявить и относиться к блокам, и показывает, как использовать блоки для упрощения общих задач, таких как перечисление набора. Для получения дополнительной информации посмотрите, что Блоки Программируют Темы.

Блочный синтаксис

Синтаксис для определения блочного литерала использует символ вставки (^), как это:

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

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

Таким же образом то, что можно использовать указатель функции для обращения к функции C, можно объявить, что переменная отслеживает блок, как это:

    void (^simpleBlock)(void);

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

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

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

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

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

    simpleBlock();

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

Блоки могут также взять параметры и возвращаемые значения точно так же, как методы и функции.

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

    double (^multiplyTwoValues)(double, double);

Соответствующий блочный литерал мог бы быть похожим на это:

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

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

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

    ^ 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);

Блоки могут получить значения от объема включения

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

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

- (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();

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

Integer is: 42

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

Используйте __ основные переменные для совместного использования хранения

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

Как пример, Вы могли бы переписать предыдущий пример как это:

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

Поскольку anInteger объявляется как a __block переменная, ее хранение совместно используется с блочным объявлением. Это означает, что вывод журнала теперь показал бы:

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);

На сей раз вывод показал бы:

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

Можно передать блоки как параметры методам или функциям

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

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

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

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

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

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

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

Объявление для 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");
    }];

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

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

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

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

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

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

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

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

Избегите Циклов Сильной ссылки при Получении сам

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

Блоки поддерживают сильные ссылки к любым полученным объектам, включая self, что означает, что просто закончиться с циклом сильной ссылки, если, например, объект поддерживает a 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

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

Для предотвращения этой проблемы это - наиболее успешная практика для получения слабой ссылки на self, как это:

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

Путем получения слабого указателя на self, блок не будет поддерживать прочные отношения назад к XYZBlockKeeper объект. Если тот объект освобожден, прежде чем блок вызывают, weakSelf указатель будет просто установлен в nil.

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

В дополнение к общим обработчикам завершения многие Какао и Касание Какао API использует блоки для упрощения общих задач, таких как перечисление набора. 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);
    }];

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

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

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

OS X и iOS предлагают множество технологий для параллелизма, включая два механизма планирования задач: очереди Работы и Центральная Отгрузка. Эти механизмы вращаются вокруг идеи очереди задач, ожидающих, чтобы быть вызванными. Вы добавляете свои блоки к очереди в порядке, Вам нужны они, чтобы быть вызванными, и система исключает их из очереди для вызова, когда процессорное время и ресурсы становятся доступными.

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

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

Очередь работы является Сенсорным подходом Какао и Какао к планированию задач. Вы создаете 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 (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() функция не возвращается, пока блок не завершил выполнение; Вы могли бы использовать его в ситуации, где параллельный блок должен ожидать другой задачи завершиться на основном потоке перед продолжением, например.

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