Блоки в Objective-C
Автор: Mike Ash; оригинал статьи: Blocks in Objective-C.
Аннотация
Слово «блок» довольно двусмысленно, так что сразу поясню, что не имею в виду ту самую составную структуру операторов, которая существовала в С от начала времен. Я говорю о предложенном Apple дополнении к языку, которое делает возможным использование анонимных функций…
Статья достаточно старая. Написана в те времена, когда спецификации по блокам еще не были опубликованы Apple. Тем не менее материал дает общее представление о блоках, приводит примеры использования. То есть те базовые вещи, которые останутся неизменными.
Блоки не так давно были анонсированы Apple для Mac OS X 10.6 Snow Leopard и iOS 4.0. Подробности реализации смотрите в документации Apple, например здесь. Обратите внимание на тонкости использования блоков при доступе ко внешним переменным, особенно к объектам Objective-C/С++. Не забывайте о том, что блоки — это самые настоящие объекты Objective-C, со всеми вытекающими. И да пребудет с вами Сила.
Итак, начнем. Для начала, типичный блок:
x = ^{ printf("hello world\n"); }
Забавная каретка (^) перед скобками — это то, что различает нашу конструкцию от классического блока составных операторов. Определив блок, мы можем просто вызвать его таким образом:
x();
Результатом будет напечатанное «hello world». Добавим пару параметров:
x = ^(int a, char *b){ printf("a is %d and b is %s", a, b); }
Теперь мы можем вызвать блок точно так, как вы и ожидаете:
x(42, "fork!");
Удалим эти параметры снова:
int a = 42;
char *b = "fork!";
x = ^{ printf("a is %d and b is %s", a, b); }
x();
Здесь показана одна из по-настоящему интересных особенностей блоков: им доступны переменные, определенные на том же уровне видимости, что и сам блок. Приведенный пример выглядит не столь захватывающе (в конце концов, почему бы не передать просто a и b в блок в качестве параметров?), истинный интерес возникает, когда мы передаем наш блок в другие функции:
int a = 42;
char *b = "fork!";
callblock(^{ printf("a is %d and b is %s", a, b); });
Когда функция callblock() вызывает наш блок, он все так же имеет доступ к локальным переменным a и b, несмотря на то, что мы не передаем их в функцию явным образом.
Согласно официальной реализации блоков от Apple такие внешние переменные в данном примере будут доступны только в режиме чтения (read-only). Для того, чтобы сделать переменные изменяемыми (mutable), их необходимо объявлять со спецификатором __block. В силу того, что блоки могут выполняться задолго после того, как произойдет выход за переделы видимости объявления внешних переменных, все затрагиваемые в блоке внешние __block переменные могут быть в любой момент «перенесены» из стека(stack) в кучу(heap), так что физический адрес такой переменной может измениться в ходе выполнения программы!
Собственно, мы почти закончили с основами теории. Только один пример — блок, возвращающий значение:
x = ^(int n){ return n + 1; };
printf("%dn", x(2));
Этот код напечатает «3». Отметим, что здесь нет необходимости декларации типа возвращаемого значания, потому что компилятор уже знает его из типа выражения оператора return.
В чем выгода? Главным преимуществом блоков является то, что они позволяют вам писать свои личные конструкции языка. Рассмотрим появившуюся в Леопарде конструкцию for(… in …) . Прекрасное дополнение к языку. Раньше нам приходилось писать кучу кода для того, чтобы пробежаться по всем значениям массива:
NSEnumerator *enumerator = [array objectEnumerator];
id obj;
while((obj = [enumerator nextObject]))
// и наконец-то мы можем делать что-то с нашим obj
Новый синтаксис сокращает все до одной строчки:
for(id obj in array)
Просто супер. Единственная проблема заключается в том, что все прежние годы мы обходились без этой конструкции. Нам пришлось ждать, пока Apple не сделала это для нас. Такого больше не случится с блоками. Да, у вас не получится добавить аналогичную конструкцию к языку, но вы можете получить то же самое удобство с методом, который напишете исключительно сами:
my_for(array, ^(id obj){ /* тут ваше тело цикла */ });
Или, в несколько более странной, но намного более интересной объектно-ориентированной форме:
[array do:^(id obj){ /* тут ваше тело цикла */ }];
Реализацию метода -do: method я оставляю читателю. Можно предложить, что она относительно проста.
Еще один пример. Рассмотрим директиву @synchronized. Перепишем ее используя блоки:
[obj synchronized:^{ /* код, защищенный вызовами lock */ }];
Ну ладно, скажете вы, но в чем, собственно, профит? После всего, после того как for/in и @synchronized уже часть языка, зачем их переписывать?
Конечно, вы не будете это делать. Это было бы глупо. То были только примеры, демонстрирующие идею: вы можете создавать свои управляющие структуры языка. Конечно, интереснее всего создавать новые структуры! Пара идей:
Открыть файл и гарантированно закрыть его по завершению работы:
[[NSFileHandle fileHandleForReadingAtPath:path]
closeWhenDone:^(NSFileHandle *handle) {
/* работаем с файлом здесь */
}];
Создаем новый массив на основе обработки членов существующего:
newArray = [existingArray map:^(id obj){
return [obj stringByAppendingString:@"suffix"];
}];
Фильтруем содержимое массива:
newArray = [existingArray filter:^(id obj){ return [obj hasPrefix:@"my"]; }];
Синхронизация с главным потоком приложения:
/* код в потоке */
PerformOnMainThread(^{ /* код, выполненный в главном потоке */ });
/* код в потоке */
Отложенное исполнение:
PerformWithDelay(5.0, ^{ /* запустится с 5-секундной задержкой */ });
Параллельная обработка:
[array doParallelized:^(id obj){
/* Выполнится на всех ядрах процессора сразу */
}];
Да кучу всего можно придумать.
Еще одно применение блоков, позволяющее сделать жизнь легче — функции обратного вызова (callbacks). Если вы достаточно кодировали под Cocoa, вам почти наверняка пришлось реализовать много такого рода функций и это было как заноза в одном месте. Попытка передать переменные на ту сторону выливалась примерно в такую мешанину:
- (void)method {
int foo;
NSString *bar;
/* делаем что-то полезное с нашими переменными */
NSDictionary *ctx = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithInt:foo], @"foo", bar, @"bar", nil];
[NSApp beginSheet:sheet
modalForWindow:window
modalDelegate:self
didEndSelector:@selector(methodSheetDidEnd:returnCode:contextInfo:)
contextInfo:ctx];
}
- (void)methodSheetDidEnd:(NSWindow *)sheet returnCode:(int)code contextInfo:(void *)ctx {
NSDictionary *ctxDict = ctx;
[ctxDict autorelease];
int foo = [[ctxDict objectforKey:@"foo"] intValue];
NSString *bar = [ctxDict objectForKey:@"bar"];
/* Еще что-нибудь творим с нашими переменными */
}
Да уж. Я удалил весь полезный код, так что все, что осталось — это обвязка. Ужасная обвязка, чье единственное предназначение — сообщить объекту о том, кто его вызвал и упаковать всю локальную информацию так, чтобы мы смогли прочитать ее по возврату из функции.
А теперь представьте себе, как выглядел бы этот код, если бы мы использовали блоки:
- (void)method {
int foo;
NSString *bar;
/* делаем что-то полезное с нашими переменными */
[sheet beginSheetModalForWindow:window didEndBlock:^(int code){
/* работаем с foo */
/* работаем с bar */
/* работаем с кодом, с листом (sheet), с окном, с чем угодно */
}];
}
Разве не здорово? Весь этот ужасный вспомогательный мусор просто улетел в окно. Структура кода внезапно стала абсолютно логичной, вы можете читать ее сверху вниз и вы можете получить доступ к любой локальной переменной, какой пожелаете.
Другой пример: сортировка массива со своей функцией сравнения, использующей некие данные (переменные) извне. NSArray реализует такую сортировку с помощью метода -sortedArrayUsingFunction:context:. Это старомодный стиль и я не буду использовать его. Он похож на предыдущий пример. Вам придется определить отдельную функцию, где-то снаружи вашего кода, но не настолько, чтобы она всем бросалась в глаза. Вам придется придумать и инициализировать некий контекст для передачи в функцию. Если вы передаете более одной переменной, вам придется передавать словарь(ассоциативный массив) с последующей его распаковкой или указатель на структуру. А вот вариант с блоками в качестве функции сравнения:
sorted = [array sortedArrayUsingBlock:^(id a, id b){
/* Сравнивай, используй локальные переменные, чувствуй себя как дома */
}];
Вот и все, что нам понадобится.
Функции обратного вызова — одна из самых мощных штук в C и Objective-C, но во многих ситуациях они могут быть исключительно трудны для понимания и неестественны. Блоки выглядят гораздо более обещающими в создании естественного и понимаемого кода.
До сих пор я показывал только примеры использования блоков, как насчет создания их? C этим дело обстоит хуже, но не намного. Единственным камнем преткновения является то, что синтаксис определения блоков несколько уродлив (он был введен после и на основе синтаксиса указателей на функции). Хотя, все не так страшно, а все остальное — весьма мило и просто. Например, вот так мы могли бы реализовать метод -map: из вышеприведенного примера:
- (NSArray *)map:(id (^)(id))block { // на входе - id, на выходе тоже id
NSMutableArray *ret = [NSMutableArray array];
for(id obj in self)
[ret addObject:block(obj)];
return ret;
}
Достаточно просто, особенно с учетом той мощи, что теперь доступна нам.
Информация по реализации блоков от Apple пока что разрознена и редка (текст был написан, когда Apple еще не опубликовала официальную документацию, сейчас информации вполне себе море, прим. ред). Немного больше деталей может быть найдено в списке рассылки Clang. Для более цельного понимания концепции блоков обратитесь к языку программирования Smalltalk, в котором блоки используются практически везде, вплоть до конструкций if/then и циклов. Я же просто надеюсь, что блоки значительно изменят стиль программирования в Snow Leopard.
Ресурсы
Об авторе оригинальной статьи. Его зовут Mike Ash (Майк Эш), он — разработчик под Mac, как он говорит сам на своей домашней страничке, работает на Rogue Amoeba ночью и летает на глайдере днем.
- Оригинал статьи: Blocks in Objective-C
- Официальная документация Apple: Introducing Blocks and Grand Central Dispatch (английский)
судя по всему, статья весьма устарела…
сейчас декларировать блок надо так (из доков Apple):
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};