Когда на 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, объявленные за пределами блока, без проблем используются внутри блока для вычисления значения выражения. В разделе о функциях обратного вызова эта возможность будет обсуждаться снова.

Блоки работают даже в классическом C
Блоки работают даже в классическом C
 

Функции обратного вызова

Далее для экономии места я не буду так подробно приводить описания блоков кода.

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

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, &param);

Теперь все выглядит еще страшнее, и ситуация будет ухудшаться, если придется передавать больше параметров. А теперь посмотрим, как та же задача решается с помощью блоков. Первое, что нужно сделать, — описать функцию:

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 все чаще и чаще использует блоки кода. И хотя сразу прийти к их использованию довольно трудно, они действительно помогают в кодировании разных событийных механизмов. Когда блоки кода появились, я не очень хорошо их воспринял. Но потом попробовал один раз, второй раз и понял, что они реально упрощают мою жизнь. Короче говоря, советую!

 

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии