Блоки в Objective-C, часть 2
Автор: Mike Ash; оригинал статьи: Practical Blocks.
Аннотация
Новая статья Майка Эша, посвященная блокам в Оbjective-C. Больше подробностей, примеров, практик использования.
Перед чтением крайне рекомендуется ознакомление с предыдущей статьей Майка, посвященной основам концепции блоков. Перевод статьи находится здесь
Вступление
Добро пожаловать в еще один выпуск Вопросов и Ответов. Я вернулся из отпуска и готов к раздаче новых слонов. В эту неделю я последую предложению Лэндона Фуллера и напишу продолжение к уже начатой на Вопросах и Ответах теме блоков, тем более что разработка блоков завершена и спецификации доступны.
Хотя Apple внедряет поддержку блоков в свои средства разработки, реализованы они были в процессе участия в разработке свободных компиляторов gcc и clang. Лэндон использовал этот код для своего проекта PLBlocks, который позволяет использовать блоки в Mac OS X 10.5. (напоминаем, поддержка блоков введена Apple начиная с Mac OS X 10.6 и iOS 4.0 — прим. переводчика).
Я не буду касаться вопросов установки или использования PLBlocks, все это великолепно описано в документации PLBlocks. Если вам нужно больше понимания о том, как работают блоки — обратитесь к документации clang.
Предполагаю также, что вы знакомы с основами блоков. Если нет — прочитайте сначала мою предыдущую статью о них.
Основы
Блоки — это объекты Objective-C. Когда вы создаете блок в своем коде, вы создаете объект — во многом по аналогии с тем, как строковая константа @"…" создает для вас объект NSString. Вы можете использовать этот объект точно таким же образом, как и любой другой объект Objective-C: отправлять ему сообщения, поместить в контейнер, передавать в качестве параметра, возвращать из метода и т.д.
Есть существенное отличие объектов-блоков от строк-констант. Объект, представляющий блок, не является одной и той неизменяемой сущностью на протяжении всего времени жизни программы. Объясняется это тем, что блок взаимодействует с окружающей средой (то есть, с объектами и структурами, находящимися на том же уровне видимости) и эта среда каждый раз разная при каждой вызове блока. Короче говоря: каждый раз, когда выполнение программы доходит до конструкции ^{…} — создается новый объект.
Создание нового объекта каждый раз при его выполнении — относительно медленный процесс, поэтому используется несколько необычный подход: объекты, которые вы получаете из конструкции ^{…} являются стековыми. Это означает, что объекты-блоки живут столько же, сколько обычные локальные переменные и уничтожаются автоматически при выходе из области видимости. Довольно странно, не так ли?
Часто бывает полезно обеспечить блоку существование и за пределами области его видимости, области где он был создан. Например, вы хотите вернуть блок из метода или сохранить его для дальнейшего использования. Для того, чтобы это заработало, вы должны скопировать блок. Делается это точно также, как копирование любого другого объекта Objective-C: через отправку сообщения copy. И, как для любого другого объекта Objective-C, в случае неиспользования автоматической сборки мусора, вы владеете созданным блоком и должны в нужный момент уничтожить его через сообщения release или autorelease.
Вот пример возврата блока из метода:
- (void (^)(void)) block { return [[^{ ... } copy] autorelease]; }
Отметим, что работа в блоке с внешними переменными по умолчанию позволяет осуществить доступ только к неизменяемым (const) копиям этих переменных. Иными словами, нижеследующий код не будет работать:
int i;
^{ i++; };
Для обхода этого ограничения используйте новое ключевое слово __block:
__block int i;
^{ i++; };
Смысл этого усложнения в том, что сопровождение локальных переменных, декларированных с модификатором __block значительно затратнее их обычных аналогов. __block-переменные имеют другую семантику применительно к указателям на объекты Objective-C (об этом чуть позже). Так что разработчики блоков решили вместо того, чтобы грести все под одну гребенку, предоставить программистам возможность выбора.
Примеры
Я собираюсь показать кучу примеров использования блоков с PLBlocks на Mac OS X 10.5. Желающие подробностей могут посмотреть на созданный мною демонстрационный проект, который можно получить с моего SVN сервера:
svn co www.mikeash.com/svn/PLBlocksPlayground/
Если вы просто хотите посмотреть на код — кликните по ссылке в вышеприведенной команде.
Свое API
Это основное, для чего предназначены блоки. Посмотрим, что мы можем сделать.
Я уже отмечал в предыдущей статье, что блоки позволяют нам создавать новые конструкции языка без необходимости модификации самого языка. Прежде, чем мы приступим к деталям, я хочу ввести небольшое упрощение — новый тип. Большинство блоков, применяемых в управляющих конструкциях языка, не имеют параметров и не возвращают никаких значений. Раз так, давайте создадим такой тип, описывающий блок без параметров и возвращаемого значения:
typedef void (^BasicBlock)(void);
В качестве простейшего примера возьмем типичную задачу Cocoa-разработки: выполнение участка кода со своим внутренним autorelease pool — для контроля за потребляемой памятью. Обычно это выглядит так:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; ... [pool release];
Все не так плохо. Правда довольно многосложно. Можно написать макро, но макроподстановки — это настоящее зло, которое часто преподносит скрытые сюрпризы. Напишем лучше маленькую функцию, используя блоки:
void WithAutoreleasePool(BasicBlock block)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
block();
[pool release];
}
И используем вот так:
for(id obj in array)
WithAutoreleasePool(^{
[self createLotsOfTemporaryObjectsWith:obj];
});
Получилось короче, легче и понятнее. Отлично!
Давайте сразимся с чем-нибудь посложнее. Другая типовая задача в Cocoa: запуск участка кода с небольшой задержкой с применением performSelector:withObject:afterDelay:. Часто мы используем нулевую задержку, имея ввиду «выполнить этот код сразу же по возврату в цикл выполнения (runloop)». Проблема в том, что такой подход требует объект и отдельный метод, а передача дополнительного контекста может быть и вовсе затрудненной. Пишем снова маленькую функцию с блоками:
void RunAfterDelay(NSTimeInterval delay, BasicBlock block)
{
[[[block copy] autorelease] performSelector:
@selector(my_callBlock) withObject: nil afterDelay: delay];
}
Обратите внимание на то, как выполнено копирование и авто-освобождение (copy/autorelease) блока с тем, чтобы он «жил» до тех пор, пока не будет выполнен. Мы используем также небольшую категорию класса NSObject для реального выполнения блока (используем тот факт, что блоки являются унаследованы от NSObject):
@implementation NSObject (BlocksAdditions)
- (void)my_callBlock
{
void (^block)(void) = (id)self;
block();
}
@end
Пример использования:
NSString * something = ...;
RunAfterDelay(0, ^{
NSLog(@"%@", something);
[self doWorkWithSomething: something];
});
Так намного проще нежели использование стандартного паттерна Cocoa.
Еще одной типовой задачей является написание критической секции для кода с помощью вызова lock. Обычно это выглядит так:
[lock lock]; ...do stuff... [lock unlock];
Однако тут могут быть проблемы. Например, если вы забудете вызвать unlock, выйдете из блока в середине его выполнения, если сгенерируется исключение — ваше приложение заблокируется (получит т.н. deadlock). Самый простой путь решения состоит в использовании конструкции @try/@finally:
[lock lock];
@try
{
...do stuff...
}
@finally
{
[lock unlock];
}
Это довольно громоздко. Мы можем подойти с другой стороны и использовать блоки для автоматизации блокировки и разблокирования:
@implementation NSLock (BlocksAdditions)
- (void)whileLocked: (BasicBlock)block
{
[self lock];
@try
{
block();
}
@finally
{
[self unlock];
}
}
@end
Это не одно и то же с предыдущим методом. Например, используя явный вызов @try/@finally вы можете вернуть значение (выйти из функции) изнутри конструкции @try и это сработает. В то же время попытка проделать такой же трюк изнутри блока не удастся, потому что мы продекларировали использование блока, не возвращающего значение (BasicBlock). Это можно обойти с помощью __block переменной, в которой мы сохраним возвращаемое значение. Мне кажется, это отличная штука, так как она позволяет воздержаться от хитровывернутого кодирования внутри критической секции, где потенциал совершения ошибок очень велик.
Стиль кодирования
Есть два интересных момента в вышеприведенном коде — по одной и той же причине. Первый момент заключается в том, что мы использовали функции, а не методы. Так как блоки являются объектами, мы могли бы использовать использовать категории для них (имеются ввиду категории для NSObject, поскольку реальный класс блока вряд ли известен, да это и неинтересно никому — прим. переводчика). Вместо использования функции RunAfterDelay мы могли бы написать метод runAfterDelay: для NSObject. Второй момент — параметр для блока всегда помещен в конец списка параметров(аргументов) функции, несмотря на то, что это самый важный параметр и его имело бы смысл поместить в начало.
Это сделано по одной и той же причине. Код должен быть читаем, даже когда мы используем многострочное тело блока. Представьте себе использование методов вместо функций:
[^{
for(id obj in array)
[^{
[self doImportantWork:obj];
} withAutoreleasePool];
} runAfterDelay: 0];
Что-то невразумительное. Сначала идет код, потом что-то, к чему этот код будет применен. При вложенности блоков вам придется читать все в порядке LIFO. В общем, это плохая идея — писать управляющие конструкции через методы. Про этой же причине, имея дело с блоком как с параметром — всегда помещайте его в конец списка параметров.
Коллекции
Использование блоков с коллекциями может дать нам более мощные циклические конструкции. Начнем с метода для NSArray — простой подстановки для цикла for:
- (void)do: (void (^)(id obj))block
{
for(id obj in self)
block(obj);
}
Это все не так интересно. В конечном итоге, все сводится к аналогу конструкции for/in, но без возможности статического приведения типов объектов (было бы неплохо привести такой пример до того, как Apple ввела свои for/in в язык). Пример использования:
NSArray *array = ...;
[array do: ^(id obj){ NSLog(@"%@", obj); }];
Не очень вдохновляет. Вот кое-что поинтереснее. Используем блок для мапирования(отображения) массива в новый:
- (NSArray *)map: (id (^)(id obj))block
{
NSMutableArray *new = [NSMutableArray array];
for(id obj in self)
{
id newObj = block(obj);
[new addObject: newObj ? newObj : [NSNull null]];
}
return new;
}
Вот как мы могли бы использовать этот код для создания массива имен на основе массива людей:
NSArray *people = ...;
NSArray *names = [people map: ^(id person){
return [NSString stringWithFormat: @"%@ %@",
[person firstName], [person lastName]];
}];
Это намного лучше, чем писать каждый раз вручную цикл. Передавая блоки, нам достаточно написать реализацию цикла только один раз и затем использовать ее многократно.
Еще один пример, фильтрация массива:
- (NSArray *)select: (BOOL (^)(id obj))block
{
NSMutableArray *new = [NSMutableArray array];
for(id obj in self)
if(block(obj))
[new addObject: obj];
return new;
}
Пример: выкидываем слишком короткие строки:
NSArray *longStrings = [strings select: ^ BOOL (id obj) { return [obj length] > 5; }];
Обратите внимание на явное указание типа возвращаемого значения. Результат операции сравнения в Си имеет тип int, а не BOOL, так что мы не можем позволить компилятору самому догадаться о типе — компилятор решит, что блок возвращает int и создаст блок несовместимого типа. Альтернативное решение — явно привести выражение в требуемому типу в операторе return.
Пример из некого GUI приложения: получение всех текстовых полей внутри отдельно взятого view:
NSArray *textFields = [[view subviews] select: ^(id obj){
return [obj isKindOfClass: [NSTextField class]];
}];
Функции обратного вызова
API, базирующееся на функциях обратного вызова (callbacks) — это то место, где блоки как никогда кстати. Вместо передачи пары селектор-делегат или указатель на функцию-указатель на контекст просто передаем блок. Это намного упрощает передачу контекста (реализация блока сама отвечает за это) и позволяет держать весь код в одном месте.
Напрашивающимся примером являются оповещения (notifications). В то время как имеет смысл создание нескольких отдельных методов для оповещений, реализация проста и может иногда дать лучший код:
@implementation NSNotificationCenter (BlocksAdditions)
- (void)addObserverForName: (NSString *)name object: (id)object
block: (void (^)(NSNotification *note))block
{
[self addObserver: [block copy]
selector: @selector(my_callBlockWithObject:)
name: name
object: object];
}
@end
Метод my_callBlockWithObject: реализован в категории NSObject, точно так же, как метод my_callBlock из предыдущих примеров, за исключением того, что он принимает на вход один параметр и передает его в блок.
Используется это так:
[[NSNotificationCenter defaultCenter] addObserverForName:
NSApplicationDidBecomeActiveNotification
object: nil
block: ^(NSNotification *note){
NSLog(@"Did become active");
}];
Здесь не реализован механизм отписки от получения оповещений. Это можно добавить, но это потребует скорее всего дополнительный контекст состояния, управляемый вызывающим объектом. Оставляю читателю в качестве домашнего упражнения.
Выпадающие окна (sheets) — хороший пример API, использующего функции обратного вызова. Вам нужно реализовать метод для вызова, сохранить текущее состояние (контекст) либо в одной большой структуре и передать ее указатель в качестве параметра, либо используя переменные объекта. Все это не очень вдохновляет.
Небольшая категория, транслирующая такой API для использования с блоками:
@implementation NSApplication (SheetAdditions)
- (void)beginSheet: (NSWindow *)sheet modalForWindow:(NSWindow *)docWindow
didEndBlock: (void (^)(NSInteger returnCode))block
{
[self beginSheet: sheet
modalForWindow: docWindow
modalDelegate: self
didEndSelector: @selector(my_blockSheetDidEnd:returnCode:contextInfo:)
contextInfo: [block copy]];
}
- (void)my_blockSheetDidEnd: (NSWindow *)sheet returnCode: (NSInteger)returnCode
contextInfo: (void *)contextInfo
{
void (^block)(NSInteger returnCode) = contextInfo;
block(returnCode);
[block release];
}
@end
Теперь вы сможете просто подставить тело блока прямо в вызов этого метода и получить прямой доступ ко всем переменным контекста.
Еще один пример API такого рода — NSURLConnection. Он работает в двух режимах — синхронном и асинхронном. Синхронный метод может быть использован только из вспомогательного потока, который может быть заблокирован на значительное время, потому что сетевые операции требуют много времени для своего выполнения. Асинхронный режим требует написания большого количества дополнительного кода. Давайте напишем метод, который вводит объект в асинхронный режим, выполняет блок по факту завершения работы, обрабатывает метаданные сетевого запроса и возможные сетевые ошибки. Реализуем это посредством использования синхронного API в фоновом потоке. Код использует две функции, RunInBackground и RunOnThread, которые представляют собой блоковое API для создания нового потока и выполнения блока в нем соответственно. Эти функции довольно просты, я не буду приводить здесь их полный вариант, но вы можете посмотреть на них в демо-проекте из SVN, если вам интересно.
Вот так примерно выглядит код:
@implementation NSURLConnection (BlocksAdditions)
+ (void)sendAsynchronousRequest: (NSURLRequest *)request
completionBlock: (void (^)(NSData *data, NSURLResponse *response, NSError *error))block
{
NSThread *originalThread = [NSThread currentThread];
RunInBackground(^{
WithAutoreleasePool(^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [self sendSynchronousRequest: request returningResponse: &response error: &error;];
RunOnThread(originalThread, NO, ^{ block(data, response, error); });
});
});
}
@end
Пара замечаний. Во-первых, обратите внимание на то, как текущий поток сохраняется в локальной переменной с тем, чтобы быть прочитанным в теле блока, выполняемого в другом потоке. Это демонстрирует пример того, как блоки могут быть использованы для быстрой передачи контекста. Во-вторых, проследите как блок, переданный в RunInBackground, завершается вызовом функции RunOnThread, которая использует другой блок для обратного вызова в исходный поток. Этот тип вызовов посредством вложенных блоков удобен для лаконичной реализации асинхронных оповещений.
Пример использования такого API:
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString: @"http://www.google.com/"]];
[NSURLConnection sendAsynchronousRequest: request
completionBlock: ^(NSData *data, NSURLResponse *response, NSError *error){
NSLog(@"data: %ld bytes response: %@ error: %@",
(long)[data length], response, error);
}
];
Очень хорошо. Использование обычного асинхронного API потребовало бы реализации нескольких методов и, вероятно, создания нового класса. Использование синхронного API, реализация всей обработки в дополнительном потоке, возврат в главный поток по завершению с возвратом полученных результатов потребовало бы намного больше строк кода, чем наш вариант.
Подводные камни
Понятно, без ложки дегтя не обойдешься. Есть несколько вещей, на которые надо обращать особое внимание при работе с блоками.
Когда блок копируется, то любые локальные указатели — переменные на объекты автоматически получают вызов retain. Соответственно, при уничтожении блока все локальные указатели на объекты получают вызов release. Это удобно для разработчика — он знает, что все ссылки на внешние объекты остаются валидными. Любая ссылка на объект self — это ссылка на локальную переменную, поэтому self также получит retain/release. Любая ссылка на объект является неявной ссылкой на self и тоже получит retain/release. В некоторых ситуациях, однако, может случиться неприятность. Представьте реализацию базирующегося на блоках оповещательного API, который позволяет отписку от оповещений. Если ваш блок ссылается на self любым способом и вы отписываетесь от оповещения стандартным для Cocoa способом — через dealloc, то ваш объект не будет удален — блок будет продолжать «держать» ссылку на него.
Простое решение этой проблемы заключается в том факте, что __block-переменные не получают вызовы retain/release от блока. Это происходит потому, что эти переменные — модифицируемые (mutable) и автоматическое управление памятью ими подразумевает генерацию соответствующего кода для каждого изменения такого объекта. Было решено, что такие вещи сложно реализовать должным образом, тем более что один и тот же блок может быть исполняем из нескольких потоков одновременно. Таким образом, вы может избегнуть циклического retain следующим образом:
__block MyClass *blockSelf = self;
^{
[blockSelf message];
[blockSelf->ivar message];
};
Типы CoreFoundation точно также требуют вызовов retain/release, когда они присвоены не-__block переменным — просто потому, что они тоже являются объектами Objective-C. Тем не менее, компилятор этого не знает. Для помощи компилятору введен новый добавлен атрибут для структур __attribute__((NSObject)), который заставляет компилятор воспринимать указатели на такие структуры как ссылки на объекты Objective-C, по крайней мере в контексте применения в блоках. Предположительно Apple добавит этот атрибут ко всем типам CFTypes в Mac OS X 10.6. Но пока что у нас Mac OS X 10.5 (статья достаточно старая — прим. переводчика), так что объекты CoreFoundation не управляются должным образом будучи «захваченными» блоком. Для корректного разруливания этой ситуации либо избегайте использования объектов CoreFoundation в блоках (декларирование переменных соответствующими toll-free NS*-аналогами — таки убедит компилятор вставить требуемые retain/release), либо убедитесь, что время жизни таких объектов CoreFoundation как минимум не короче, чем время жизни блока.
Еще одна ловушка с блоками заключается в том, что они — стековые объекты. Использование конструкции ^{…} абсолютно аналогично декларации локальной переменной и взятие ее адреса с целью последующего использования. Вы можете делать что угодно с этим адресом, но как только управление выйдет за пределы видимости этой локальной переменной, адрес не будет больше иметь смысла. Так, нижеследующий выглядящий невинно пример не будет работать:
BasicBlock block;
if(condition)
block = ^{...};
else
block = ^{...};
Тело оператора if (равно как и конструкции else) находится в отдельных областях видимости и все локальные объекты в нем будут уничтожены при выходе из конструкции if/else. Проблема решается через копирование:
BasicBlock block;
if(condition)
block = [[^{...} copy] autorelease];
else
block = [[^{...} copy] autorelease];
Главная опасность заключается не в том, что это тяжело исправить (совсем нет), но в том, что совсем не очевидно понять, когда это нужно сделать.
Завершение
На этом закончим. Вы увидели, как заставить блоки работать, несколько полезных примеров их использования и кое-какие подводные ловушки использования блоков. Сейчас мы уже готовы использовать блоки прямо в Mac OX X 10.5, не дожидаясь отгрузки 10.6.
Ресурсы
Об авторе оригинальной статьи. Его зовут Mike Ash (Майк Эш), он — разработчик под Mac, как он говорит сам на своей домашней страничке, работает на Rogue Amoeba ночью и летает на глайдере днем.
- Оригинал статьи: Practical Blocks
- Официальная документация Apple: Introducing Blocks and Grand Central Dispatch (английский)