Содержание статьи
Когда на Mac OS Х и iOS появились блоки кода, программисты сказали: «о, круто», но продолжили писать код привычным способом. Может быть, они просто не поняли, насколько это круто?
Что такое «блоки кода»? Блоки кода — это типичные объекты Objective-C. Исторически официально они появились в Mac OS X 10.6 и одновременно в iOS 4, но неофициально были доступны немногим ранее. Это не функции, не методы, это просто несколько операторов, которые объединены, как нам подсказывает Капитан Очевидность, в блок. Блоки кода очень похожи на анонимные функции или лямбда-выражения. Вместе с тем они имеют некоторое сходство с указателями на функции, при этом, когда используешь первые, код получается более элегантным. Они очень удобны при создании анимации или любой другой асинхронной работе. Между тем первоначальное их предназначение — сократить объем необходимого для написания кода, совместив логически связанные куски, например код регистрации оповещения и реализации обрабатывающего метода. Давай посмотрим на несколько программистских трюков, которые покажут все прелести блоков кода.
Как и когда?
Как я уже говорил, к таким задачам относятся анимации или любые другие случаи, когда нужно передать метод, состоящий из нескольких строк кода. Когда кода больше, я бы все же создал полноценную функцию (красивее и удобнее) и вызывал бы ее из блока. Но все зависит от конкретной ситуации.
В дальнейшем мы будем работать над консольным приложением, чтобы не отвлекаться от главной темы повествования.
Объявление блока кода
Запусти на своем маке Xcode, создай проект командной строки — Command Line для OS X.
Так как мы будем кодить на Objective-C, на следующей странице мастера выбери тип проекта Foundation. Это название повелось от используемой базовой библиотеки.
При объявлении блока кода вместо имени ставится «знак вставки» — ^. Подобно обычным функциям, блоки кода могут получать параметры и возвращать аргумент. К примеру, вот как может выглядеть блок кода, получающий три параметра:
^(double slag1, double slag2, double power) {
double res = (slag1 + slag2) * power;
return res;
}
Собственно, комментарии по выполнению данного блока кода будут излишни. Заметь, пока данный код не обязан компилиться.
Если блок должен возвращать значение, тогда, подобно объявлению функции, в начале описания блока должен стоять возвращаемый им тип данных.
Замечательная особенность блоков кода — это возможность их сохранения в переменные особого типа. Чтобы объявить блочную переменную, надо написать:
double (^expression) (double, double, double);
Здесь на первом месте указан возвращаемый тип, затем символ «домик» предваряет имя объявляемой переменной, дальше в скобках указаны параметры, принимаемые блоком кода. Обрати внимание, объявление блочной переменной соответствует привязываемому к ней блоку кода по параметрам и аргументам.
Таким образом, имя блочной переменной является подобием имени функции, только с переменной ты можешь выполнить более широкий круг задач.
Теперь, когда блок кода и блочная переменная сходятся по аргументам и параметрам, мы можем присвоить блок переменной. Для этого надо ввести следующий код:
expression = ^double(double slag1, double slag2, double power) {
double res = (slag1 + slag2) * power;
return res;
};
Чтобы воспользоваться вычислениями, хранящимися в блочной переменной, достаточно написать:
NSLog(@ "%f",expression(2.0,1.0,2.0));
Программа выведет на консоль результат выражения, вычисляемого в блоке кода.
Блоки кода и куча
По своей природе блоки кода хранятся в стеке, поэтому при выходе из области видимости они удаляются, так же как и локальные переменные. Чтобы убрать это ограничение, другими словами переместить блок кода в кучу, его надо туда скопировать, отправив блочной переменной соответствующее сообщение. Например, для приведенного случая данный оборот будет выглядеть следующим образом:
expression = [expression copy];
На месте принимающей значение переменной должен быть либо член класса, либо глобальная переменная, рамки жизни которой шире, чем копируемой, иначе не было бы смысла в копировании. При этом блок кода, по сути, выполняет действия с локальными переменными, поэтому во время копирования блока в кучу вместе с ним копируются также переменные. Однако попавшие при копировании вместе с блоком кода в кучу переменные становятся немодифицируемыми. Для того чтобы они не превратились в константы, при объявлении внешних переменных тип данных надо предварять ключевым словом __block (заметь, с двумя символами подчеркивания в начале слова).
Такая же участь ждет указатели: объекты, на которые они указывают, будут существовать, пока существует данный блок кода.
Внешние переменные
В блоках кода могут использоваться внешние по отношению к ним переменные, то есть объявленные не только внутри блока, но и на том уровне, на котором объявлен сам блок. Пример:
int a = 10;
int b = 5;
int (^abc) ();
abc = ^(int n) {
int c = a + b;
NSLog(@"%d", c);
return c;
};
abc();
В данном случае переменные a и b, объявленные за пределами блока, без проблем используются внутри блока для вычисления значения выражения. В разделе о функциях обратного вызова эта возможность будет обсуждаться снова.
Функции обратного вызова
Далее для экономии места я не буду так подробно приводить описания блоков кода.
Классический пример — функция обратного вызова. Подобные вещи встречаются очень часто, особенно в многопоточных программах. Простейший вариант создания функции обратного вызова может выглядеть так:
void callbackFunc( void (*func)() ) {
func();
}
void WeWillCallYou() {
printf("Classic callback\n");
}
Сначала мы объявляем функцию callbackFunc, в качестве параметра она принимает другую функцию, которую мы и будем вызывать из своего потока или просто из своего кода.
Потом мы создаем функцию, которую мы и будем вызывать, — WeWillCallYou. А вот сам вызов будет выглядеть так:
callbackFunc(WeWillCallYou);
Немного громоздко, не правда ли? Станет еще хуже, если нужно будет передавать в функцию параметр, что бывает очень часто. Изменим нашу функцию и добавим в нее параметр:
void WeWillCallYou(int* param) {
printf("Classic callback %d\n", *param);
}
Теперь вызов этой функции может выглядеть так:
void callbackFunc( void (*func)(int *), int* param) {
func(param);
}
Изменилось объявление, потому что нам теперь нужно передавать указатель на число.
Теперь мы можем вызвать функцию и передать ей значение:
int param = 10;
callbackFunc(WeWillCallYou, ¶m);
Теперь все выглядит еще страшнее, и ситуация будет ухудшаться, если придется передавать больше параметров. А теперь посмотрим, как та же задача решается с помощью блоков. Первое, что нужно сделать, — описать функцию:
void callbackBlockFunc( void (^func)(void) ) {
func();
}
В данном случае мы создаем callbackBlockFunc, которая принимает в качестве параметра функцию и вызывает ее.
Все. Больше ничего делать не нужно. Теперь можно использовать наше описание следующим образом:
int variable = 1;
callbackBlockFunc( ^{
printf("Block %d\n", variable);
});
Я сразу же усложнил пример и добавил переменную, которая должна быть доступна внутри функции. И она доступна, хотя мы ее не передавали, потому что блок кода будет выполняться в том же контексте. Чудо? Именно так!
Многозадачность и блоки кода
Движемся к чему-то более современному и интересному — многозадачности. С помощью блоков очень просто создавать отдельные задачи, которые будут выполняться параллельно с основной.
Теперь создание отдельной задачи, которая может выполняться на отдельном ядре, может выглядеть так:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; i++ ) {
[queue addOperationWithBlock:^{
NSLog(@"test %d\n", i);
}];
}
NSLog(@"test");
[queue waitUntilAllOperationsAreFinished];
Здесь мы сначала инициализируем объект NSOperationQueue, который как раз умеет создавать отдельные операции. У этого объекта есть метод addOperationWithBlock, принимающий блок кода, который должен выполняться отдельно от основного потока. В этом примере я ограничился созданием только одного блока, но их может быть несколько.
Чтобы дождаться окончания выполнения операций, которые выполняются в очереди, я вызываю метод waitUntilAllOperationsAreFinished.
В результате выполнения данного кода в консоли может оказаться что-то типа
2013-12-09 18:14:37.191 Test[1068:1a07] test 0
2013-12-09 18:14:37.191 Test[1068:303] test
2013-12-09 18:14:37.191 Test[1068:2d0f] test 2
2013-12-09 18:14:37.191 Test[1068:350b] test 1
2013-12-09 18:14:37.191 Test[1068:1003] test 3
2013-12-09 18:14:37.195 Test[1068:2d0f] test 5
2013-12-09 18:14:37.195 Test[1068:1a07] test 4
2013-12-09 18:14:37.195 Test[1068:350b] test 6
2013-12-09 18:14:37.196 Test[1068:1003] test 7
2013-12-09 18:14:37.196 Test[1068:2d0f] test 8
2013-12-09 18:14:37.196 Test[1068:1a07] test 9
Выполнив код еще раз, в консоли можно будет увидеть, что код выполнялся в другой последовательности, — решения планировщика неисповедимы (хотя и вполне естественны).
И обрати внимание, что из блока кода мы получаем доступ к переменной i, которая на самом деле объявлена за пределами этого блока. То есть выполнение опять же идет в контексте родительского кода, что является объективным плюсом.
Итого
В своих софтверных продуктах Apple все чаще и чаще использует блоки кода. И хотя сразу прийти к их использованию довольно трудно, они действительно помогают в кодировании разных событийных механизмов. Когда блоки кода появились, я не очень хорошо их воспринял. Но потом попробовал один раз, второй раз и понял, что они реально упрощают мою жизнь. Короче говоря, советую!