Здравствуйте!
— Не хотите ли войти
Tags Switch

Блоки в Objective-C

25 июня 2010
Уровень сложности: для разработчиков с небольшим опытом

Автор: 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 ночью и летает на глайдере днем.

Комментарии к документу
Зарегистрируйтесь или войдите, чтобы оставить комментарий.
Мурад, спасибо! Ждём ещё!
Tom

судя по всему, статья весьма устарела…
сейчас декларировать блок надо так (из доков Apple):

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};

kotik
© 2009-2012, ООО «Инру»
Вход
Имя пользователя:
Пароль:
Или…
Twi
Отмена
Войти
Восстановить забытый пароль…