Необычность синтаксиса блоков повергает некоторых начинающий iOS/Mac-разработчиков в ужас. Одной из причин этого является то, что затруднительно пользоваться этим синтаксисом, слабо понимая, откуда он взялся. Несколько дней назад я натолкнулся на замечательную статью Нилса Хайята об истоках синтаксиса блоков в Objective-C. Ниже представлен мой перевод этой полезной во всех отношениях статьи.

В этой статье я хочу начать с простейших объявлений переменных в C, а затем наращивать сложность до тех пор, пока мы не придем к синтаксису блоков в Objective-C. Мне потребовалось некоторое время, чтобы привыкнуть к синтаксису блоков, но, после того, как вы поймете, как этот синтаксис устроен и как он появился, вам больше не придется гуглить, как именно нужно объявлять блок.

Если вы хотите научиться писать объявления блоков из головы, продолжайте читать!

Объявление переменных

Переменные в C (и в Objective-C) нужно объявлять.

Объявление переменных преследует две цели:

  1. Указать тип объявляемой переменной (что именно компилятор должен найти по адресу этой переменной в памяти).
  2. Дать объявляемой переменной имя, чтобы сделать ее доступной в определенной области видимости (scope).

Начнем с самого простого объявления:

int a;

Скорее всего, эта строчка была одной из первых строк кода на C, которую вы когда-либо писали.

int это базовый тип и a это имя переменной или идентификатор. (В черновике ANSI-C на странице 35 указано: “Идентификатор именует объект; функцию; тэг; экземпляр структуры, union, перечисления; typedef-имя; имя макроса; параметр макроса.”)

Для того, чтобы понять, что именно объявлено, чтение объявления начинают с идентификатора переменной, а затем смотрят, что находится правее него настолько, насколько это возможно. После этого смотрят, что находится слева от идентификатора (мы объясним, что это означает чуть ниже). (Как ни странно, Microsoft является единственной компанией, которая написала хорошую статью, объясняющую, как читать сложные объявления переменных.)

В данном случае справа от объявляемой переменной ничего нет, поэтому все просто: переменная a с типом int.

Объявление может иметь только 1 базовый тип, он определяется самым первым словом объявления.

Базовый тип объявления может быть преобразован в производные типы (derived types). Для этого используются 4 модификатора (3 из стандарта ANSI-C и 1 из предложенного Apple расширения стандарта) - *, [], () и ^.

3 модификатора ANSI-C

Модификатор указателя *

int *a;

Базовый тип по-прежнему int, а переменная по-прежнему называется a. Но модификатор указателя * говорит нам, что a - это указатель на переменную типа int, а не переменная типа int.

Модификатор * всегда находится слева от модифицируемой переменной.

Модификатор массива []

int a[];

Здесь мы видим, как модификатор [] говорит нам, что a теперь является массивом значений типа int, а не переменной типа int. Объявление может быть дополнено указанием размерности массива, например int a[10];.

Модификатор [] всегда находится справа от модифицируемой переменной.

Модификатор функции ()

int f();

Модификатор функции () говорит нам о том, что f - это функция, возвращающая значение с типом int. Этот модификатор так же может задавать аргументы, принимаемые функцией, например, int f(long); - это функция, принимающая аргумент типа long и возвращающая значение типа int.

Модификатор () всегда находится справа от модифицируемой переменной.

Комбинируем модификаторы

Указатели и массивы

Модификаторы могут использоваться совместно для создания более сложных типов. По аналогии со старшинством арифметических операций (* и / выполняются до + и -), модификаторы имеют свое старшинство. [] и () имеют более высокий приоритет над *^). Поскольку два модификатора, имеющих более высокий приоритет, пишутся справа от имени переменной, читая сложное объявление переменной вы всегда начинаете с идентификатора переменной и идете вправо насколько это возможно, после чего смотрите, что находится слева от идентификатора переменной.

Например рассмотрим такое объявление:

int *a[];

Для улучшения читаемости мы можем добавить скобки:

int *(a[]);

Это будет массив указателей на переменные типа int.

Вы можете спросить, что если нам нужен указатель на массив переменных типа int? Поскольку * ниже по старшинству, чем [], нам потребуются скобки:

int (*a)[];

Здесь a - это указатель на массив переменных типа int.

Массивы и функции

Нельзя создать массив функций, кроме того, функция не может возвращать массив или другую функцию. (В черновике стандарта ANSI-C на странице 133 говорится: “Объявление функции не должно определять возвращаемое значение типа функции или массива.”) Впрочем, функция может принимать массив в качестве аргумента.

int f(int [10]);

Здесь f - это функция, которая принимает на вход массив из десяти элементов типа int и возвращает значение типа int.

Указатели и функции

int *f();
int *(f());

В обоих случаях f - это функция, которая возвращает указатель на значение типа int.

А что если мы хотим получить указатель на функцию? Нужны скобки!

int (*f)();

f - это указатель на функцию, которая возвращает значение типа int.

Модификатор блокового (или замыкающего) указателя ^

Apple представила четвертый модификатор в своем предложении по расширению стандарта ANSI-C: ^. Этот модификатор называется модификатором блокового указателя (block pointer modifier) (или, как его сначала называли, модификатором замыкания (closure modifier)). Блоки очень похожи на указатели для функций. Объявлять блок необходимо точно так же, как мы объявляем указатель на функцию.

Модификатор блокового указателя может быть использован только с именем функции (нельзя писать int ^a, результат такой записи не определен).

Вот почему int ^b() является неверной записью и вызовет ошибку компилирования: если читать это объявление по правилам старшинства, b будет функцией, которая возвращает блоковый указатель на значение типа int. Такой сущности не может быть, поэтому при объявлении блока необходимо всегда помещать значок ^ и идентификатор в скобки.

int (^b)()

Здесь, b - это блоковый указатель на функцию, которая возвращает значение с типом int, или, говоря короче, блок, возвращающий значение с типом int.

Само собой, можно указывать аргументы, которые блок принимает на вход:

int (^b)(long)

Выше представлен блок, который принимает на вход аргумент типа long и возвращает значение с типом int.

Вот откуда появился синтаксис объявления блоков.

Вы знаете, что существуют и другие виды синтаксиса, о которых нужно помнить при работе с блоками: один из них используется для создания блоковых литералов (block literal), другой - для передачи блоков в методы Objective-C.

Неявное описание (Abstract declarator)

Объявление переменной состоит из двух элементов: неявного описания и идентификатора переменной, подставляемого в это неявное описание.

Неявное описание в стандартном C используется в трех случаях:

  • В приведении типов: в int *a; long *b = (long *)a;, (long *) - это неявное описание указателя на переменную типа long.

  • В качестве аргумента функции sizeof(): malloc(sizeof(long *));.

  • При объявлении типов аргументов функции: int f(long *);.

В Objective-C неявные описания используются еще в одном случае: при объявлени аргументов или типов возвращаемых значений методов.

- (long **)methodWithArgument:(int *)a;

Здесь и long ** и int * являются неявными описаниями.

Итак, для того, чтобы использовать блоки в качестве аргументов или типов возвращаемых значений в методах Objective-C, нам необходимо найти неявное описание для этих блоков. Это достигается удалением идентификатора из объявления блока:

int (^b)() становится int (^)(), а int (^b)(long) становится int (^)(long).

Например:

- (void)methodWithArgument:(int(^)())block;
- (void)anotherMethodWithArgument:(void(^)(long arg1))block;

Несмотря на то, что именовать аргументы блоков в неявных описаниях не обязательно, будет хорошей идеей все-таки это делать. Это будет хорошей подсказкой касательно того, что блок ожидает получить в качестве аргументов.

Блоковые литералы

Когда вы пишите int a = 2;, int a является объявлением переменной, а 2 - это литерал целочисленного типа.

Значок ^ может быть использован и как унарный оператор для трансформации реализации функции (function implementation) в блок. (Из документации по Clang: “Блоковый литерал создает ссылку на блок. Это делается с использованием значка ^ в качестве унарного оператора.”) Вам не обязательно задавать тип возвращаемого значения блока (это зависит от того, что будет возвращено в return блока), но при желании это можно сделать.

При объявлении блокового литерала необходимо именовать его аргументы, чтобы их можно было использовать в реализации блока. Поэтому для блока int (^block)(long, long); блоковый литерал будет выглядеть так:

block = ^(long a, long b) {
  int c = a + b;
  return c;
}

Заключение

Несмотря на то, что блоки выглядят сложными в использовании, их синтаксис в Objective-C основан на стандартном синтаксисе C. Блок в Objective-C - это ни что иное, как указатель на функцию, которая получает область видимости этого указателя. Как только вы это поймете, а также попрактикуетесь в написании и чтении объявлений блоков, вы найдете блоки гораздо более простыми в понимании.

Примечание переводчика: Я настоятельно рекомендую всем ознакомится с оригиналом заметки на английском, поскольку английская терминология в отношении данной тематики гораздо короче и понятнее русской. При возникновении каких-либо замечаний по переводу, прошу высылать их мне на почту - max[at]maxmikheev.org