Статьи

Понимание блоков Objective-C

Цель этого руководства — дать краткое представление о блоках Objective-C, уделяя особое внимание их синтаксису, а также изучить идеи и шаблоны реализации, которые делают эти блоки возможными.

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

  1. Блоки имеют несколько загадочный и «прикольный» синтаксис. Этот синтаксис унаследован от указателей на функции как часть C-корней Objective-C. Если вы не занимались программированием на чистом C, то, скорее всего, вам не приходилось использовать указатели на функции, и синтаксис блоков может показаться немного пугающим.
  2. Блоки порождают «идиомы программирования», основанные на функциональном стиле программирования, с которым незнаком типичный разработчик, обладающий в значительной степени обязательным опытом в стиле программирования.

Проще говоря, блоки выглядят странно и используются странным образом. Я надеюсь, что после прочтения этой статьи, ни то, ни другое не останется для вас верным!

Следует признать, что в iOS SDK можно использовать блоки без глубокого понимания их синтаксиса или семантики; блоки вошли в SDK начиная с iOS 4, и API нескольких важных платформ и классов предоставляет методы, которые принимают блоки в качестве параметров: Grand Central Dispatch (GCD), UIView основе UIView и перечисление NSArray , и многие другие. Все, что вам нужно сделать, это подражать или адаптировать некоторый пример кода, и все готово.

Однако, без должного понимания блоков, вы будете ограничены в их использовании этим шаблоном метод-берет-блок-аргумент, и вы сможете использовать их только тогда, когда Apple включила их в SDK. И наоборот, лучшее понимание блоков позволит вам использовать их мощь и откроет дверь к открытию новых шаблонов проектирования, которые они могут применять в своем собственном коде.


Поскольку в этом учебном пособии мы обсудим основные концепции блоков, которые применяются к последним версиям Mac OS X и iOS, большая часть учебного кода может быть запущена из проекта командной строки Mac OS X.

Чтобы создать проект командной строки, выберите OS X> Приложение в левой панели и выберите параметр «Инструмент командной строки» в окне, которое появляется при создании нового проекта.

новый проект

Дайте вашему проекту любое имя. Убедитесь, что ARC (автоматический подсчет ссылок) включен!

ARC

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

Исключением является конец учебника, где мы создаем интересный подкласс UIView , для которого вам, очевидно, необходимо создать проект iOS. У вас не должно возникнуть проблем с написанием игрушечного приложения для тестирования класса, но, тем не менее, вы сможете загрузить пример кода для этого проекта, если хотите.


Вы когда-нибудь слышали о странном животном под названием утконос? Википедия описывает его как «яйцекладку, ядовитое, с уткой, млекопитающее с бобровым хвостом». Блоки немного похожи на утконоса в мире Objective-C в том, что они разделяют аспекты переменных, объектов и функций. Посмотрим как:

«Литерал блока» очень похож на тело функции, за исключением некоторых незначительных различий. Вот как выглядит литерал блока:

1
2
3
4
5
^(double a, double b) // the caret represents a block literal.
{
   double c = a + b;
   return c;
}

Итак, с точки зрения синтаксиса, каковы различия по сравнению с определениями функций?

  • Блок-литерал является «анонимным» (т.е. безымянным)
  • Символ каретки (^)
  • Нам не нужно было указывать тип возвращаемого значения — компилятор может «вывести» его. Мы могли бы явно упомянуть об этом, если бы захотели.

Есть еще кое-что, но это то, что должно быть для нас очевидно.

Наш блочный литерал инкапсулирует кучу кода. Вы можете сказать, что это то, что делает функция, и вы были бы правы, поэтому, чтобы увидеть, на что еще способны блоки, читайте дальше!

Указатель блока позволяет нам обрабатывать и хранить блоки, так что мы можем передавать блоки функциям или иметь функции, возвращающие блоки — вещи, которые мы обычно делаем с переменными и объектами. Если вы уже освоили использование указателей на функции, вы сразу заметите, что эти пункты применимы и к указателям на функции, что абсолютно верно. Вы скоро обнаружите, что блоки похожи на указатели функций «на стероидах»! Если вы не знакомы с указателями на функции, не волнуйтесь, я не думаю, что вы знаете о них. Я не буду вдаваться в указатели функций отдельно, потому что все, что может быть достигнуто с помощью указателей функций, может быть достигнуто с помощью блоков — и даже больше! — поэтому отдельное обсуждение указателей на функции будет только повторяющимся и запутанным.

1
2
3
4
double(^g)(double, double) = ^(double a, double b)
{
    double c = a + b;
};

Приведенное выше утверждение является ключевым и заслуживает тщательного изучения.

Правая сторона — это блочный литерал, который мы видели минуту назад.

С левой стороны мы создали указатель блока с именем g . Если мы хотим быть педантичными, мы скажем, что ‘^’ слева означает указатель блока, тогда как тот, что справа, помечает литерал блока. Указателю блока должен быть присвоен тип, который совпадает с типом литерала блока, на который он указывает. Давайте представим этот тип как double (^) (double, double) . Однако, рассматривая тип таким образом, мы должны заметить, что переменная ( f ) «заключена» в своем типе, поэтому объявление необходимо прочитать наизнанку. Я расскажу немного подробнее о «типе» функций и блоков чуть позже.

«Указание» устанавливается через оператор присваивания «=».

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

1
2
3
4
char ch // ch identifies a variable of type char
        = // that we assigned
          ‘a’ // the character literal value ‘a’.
             ;

Конечно, вышеприведенное утверждение не включает указатели, поэтому остерегайтесь этого при проведении семантических сравнений.

Я хочу «доить» предыдущее сравнение немного больше, просто чтобы вы начали видеть блоки в том же свете, что и наше скромное выражение char ch = 'a' :

Мы могли бы разделить объявление указателя блока и присваивание:

1
2
3
4
5
double (^g)(double, double);
g = ^(double m, double n) { return m * n;
// the above is like doing:
// char ch;
// ch = ‘a’;

Мы могли бы переназначить g чтобы указать на что-то еще (хотя на этот раз аналогия с указателем или ссылкой на экземпляр объекта):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
double (^g)(double, double);
g = ^(double m, double n) { return a + b;
 
// .. later
g = ^(double x, double y) { return x * y;
 
// but not:
// g = ^(int x) { return x + 1;
 
double (^h)(double, double) = g;
 
// compare with:
int i = 10, j = 11;
int *ptrToInt;
ptrToInt = &i;
// later…
ptrToInt = &l // ptrToInt now points to j
float f = 3.14;
// ptrToInt = &f;
 
int *anotherPtrToInt;
anotherPtrToInt = ptrToInt;

Другое ключевое отличие между обычными функциями и блоками состоит в том, что функции должны быть определены в «глобальной области видимости» программы, то есть функция не может быть определена внутри тела другой функции!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
int sum(int, int);
 
//…
 
 
int main()
{
   //…
// we are *inside* main.
   int s = sum(5, 4);
}
 
 
int sum(int a, int b) // This is the function definition.
{
   int c = a + b;
   return c;
// we’re *inside* the function body for sum().
}

Большая сила блоков заключается в том, что они могут быть определены везде, где может переменная! Сравните то, что мы только что видели с функциями, с тем, что мы можем сделать с блоками:

1
2
3
4
5
int main()
{
// inside main().
   int (^sum)(int, int) = ^(int a, int b) { return a + b;
}

Снова, это помогает вспомнить аналогию char ch = 'a' здесь; присваивание переменной обычно происходит в пределах функции. За исключением случаев, когда мы определяем глобальную переменную, то есть. Хотя мы могли бы сделать то же самое с блоками — но тогда не было бы никакой практической разницы между блоками и функциями, так что это не очень интересно!

Пока что мы только посмотрели, как определяются блоки и как назначаются их указатели. На самом деле мы их еще не использовали . Важно, чтобы вы поняли это первым! На самом деле, если вы введете приведенный выше код в Xcode, он будет жаловаться, что переменная sum не используется.

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

01
02
03
04
05
06
07
08
09
10
#import <Foundation/Foundation.h>
 
int main()
{
   int (^sum)(int, int) = ^(int a, int b) { return a + b;
 
   double x = 5.1, y = 7.3;
   double s = sum(5.1, 7.3);
   NSLog(@»the sum of %f and %f is %f», x, y, s);
}

На самом деле, мы можем сделать даже лучше, чем это: мы можем определить и вызвать наш блок за один раз.

Обратите внимание на следующее:

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>
 
int main()
{
   double x = 5.1, y = 7.3;
   double s = ^(double x, double y) { return x + y;
   NSLog(@»the sum of %f and %f is %f», x, y, s);
}

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

Давайте сначала попробуем наши навыки написания синтаксиса блоков. Можете ли вы объявить указатель блока для блока, который принимает указатель на целое число в качестве параметра, а затем возвращает символ?

1
char(^b)(int *);

Хорошо, теперь как насчет определения блока, который не принимает параметров, не возвращает значения и инкапсулирует код для вывода «Hello, World!» на консоль (при вызове)? Назначьте его на указатель блока.

1
2
void (^hw)(void) = ^{NSLog(@»hello, world!»);
// block literal could also be written as ^(void){ NSLog(@»hello, world!»);

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

Вопрос: будет ли указанная выше строка выводить что-либо на консоль?

Ответ: Нет. Сначала нам нужно вызвать (или вызвать) блок, например так:

1
hw();

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

Можете ли вы написать прототип обычной функции, которая принимает блок того же типа, что и блок «Hello, world», который мы определили выше, и ничего не возвращает? Напомним, что прототип просто информирует компилятор о сигнатуре функции, определение которой он должен увидеть в более поздний момент. Например, double sum(double, double); объявляет функцию sum, принимающую два double значения и возвращающую double . Тем не менее, достаточно указать тип аргументов и возвращаемое значение без указания имени:

1
void func(void (^) (void));

Давайте напишем простую реализацию (определение) для нашей функции.

1
2
3
4
5
void func(void (^b) (void)) // we do need to use an identifier in the parameter list now, of course
{
   NSLog(@»going to invoke the passed in block now.»);
   b();
}

С риском повторения слишком много раз, func — обычная функция, поэтому ее определение должно быть вне тела любой другой функции (например, она не может быть внутри main ()).

Можете ли вы написать небольшую программу, которая вызывает эту функцию в main (), передавая наш блок печати «Hello, World»?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
#include <Foundation/Foundation.h>
 
void func(void (^b) void) // if the function definition appears before it is called, then no need for a separate prototype
{
   NSLog(@»going to invoke the passed in block now.»);
   b();
}
 
int main()
{
   void (^hw)(void) = ^{NSLog(@»hello, world!»);
   func(hw);
}
 
// The log will show:
// going to invoke the passed in block now.
// hello, world!

Нужно ли было сначала создать указатель блока, чтобы мы могли передать блок?
Ответ — нет! Мы могли бы сделать это встроенным образом следующим образом:

1
2
// code in main():
func(^{NSLog(@»goodbye, world!»); });

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

1
2
int t = 1;
int g = ff(^(int *x) { return ((*x) + 1); }, t);

Это упражнение в том, чтобы не запугать синтаксисом! Предполагая, что предупреждения не генерируются, ff имеет возвращаемый тип int (потому что его возвращаемое значение присваивается g , который является int ). Итак, у нас есть int f(/* mystery */) Как насчет параметров?

Обратите внимание, что функция вызывается с помощью встроенного блока, из которого исходит нехватка. Давайте абстрагируем это и представим это как «blk». Теперь выражение выглядит как int g = ff(blk, t); Ясно, что ff принимает два параметра, вторым является int (так как t был int). Таким образом, мы предварительно говорим: int ff(type_of_block, int) где нам нужно только определить тип блока. Чтобы сделать это, напомним, что знание типа блока влечет за собой знание типов его параметров и его возвращаемого типа (и это все). Понятно, что блок принимает один параметр типа int * (указатель на int ). Что насчет типа возврата? Давайте сделаем вывод, как это сделал бы компилятор: *x разыменовывает указатель на int , так что получается int , добавляя к которому также int.

Убедитесь, что вы не перепутали значение * в int *x со значением * в выражении *Икс. В первом это означает, что «x является указателем на переменную int» в контексте объявления типа, в то время как второе извлекает значение int хранящееся по адресу x.

Итак, наш блок возвращает int. Таким образом, type_of_block имеет type_of_block int(^)(int *) , что означает прототип ff:

1
int ff(int (^) (int *), int);

Вопрос: Могли ли мы передать блок печати «здравствуй, мир», который мы недавно создали, в ff ?

Ответ: Конечно, нет, его тип был void(^)(void)) , который отличается от типа блока, который принимает ff .

Я хочу вкратце отвлечься и поговорить немного о чем-то, что мы неявно использовали: о «типе» блока. Мы определили тип блока, который должен быть определен типами и числом его аргументов и типом его возвращаемого значения. Почему это разумное определение? Во-первых, имейте в виду, что блок вызывается так же, как функция, поэтому давайте поговорим о функциях.

Программа AC — это просто набор функций, которые вызывают друг друга: с точки зрения любой функции все остальные функции в программах являются «черными ящиками». Все, что вызывает вызывающая функция (с точки зрения синтаксической корректности), это количество аргументов, которые принимает вызываемый объект, типы этих аргументов и тип значения, возвращаемого вызываемым объектом. Мы могли бы поменять одно тело функции на другое, имеющее тот же тип и то же количество аргументов и тот же тип возврата, и тогда вызывающая функция была бы не мудрее. И наоборот, если какой-либо из них будет другим, мы не сможем заменить одну функцию другой. Это должно убедить вас (если вам нужно было убедить!), Что наша идея о том, что представляет собой тип функции или блока, является правильной. Блоки усиливают эту идею еще сильнее. Как мы видели, мы могли передать анонимный блок функции, определенной на лету, при условии, что типы (как мы их определили!) Совпадают.

Теперь мы можем поговорить о функциях, которые возвращают блоки! Это еще интереснее. Если вы думаете об этом, по сути вы пишете функцию, которая возвращает код вызывающей стороне! Так как «возвращаемый код» будет в форме указателя блока (который будет вызываться именно как функция), у нас фактически будет функция, которая может возвращать различные функции (фактически, блоки).

Давайте напишем функцию, которая принимает целочисленные параметры, представляющие различные типы бинарных операций (например, сложение, вычитание, умножение и т. Д.), И возвращает «блок калькулятора», который можно применить к операндам, чтобы получить результат этой операции.

Под бинарной операцией мы просто подразумеваем операцию, которая оперирует двумя значениями, известными как «операнды».

Предположим, мы имеем дело с double операндами. Наши расчеты также возвращают double . Какой тип нашего блока бинарных операций? Легко! Это будет double(^) (double, double) .

Наша функция (назовем ее «operation_creator») принимает int который кодирует тип операции, которую мы хотим вернуть. Так, например, вызов operation_creator(0) вернул бы блок, способный к выполнению сложения, operation_creator(1) дал бы блок вычитания и т. Д. Таким образом, объявление operation_creator выглядит как return-type operation_creator(int) . Мы только что сказали, что тип возвращаемого значения double (^)(double, double) . Как мы соединяем эти два вместе? Синтаксис становится немного волосатым, но не паникуйте:

1
double (^operation_creator(int)) (double, double);

Не позволяйте этой декларации одолеть вас! Представьте, что вы только что наткнулись на это объявление в каком-то коде и хотели его расшифровать. Вы можете деконструировать это так:

  1. Единственный идентификатор (имя) — operation_creator . Начни с этого.
  2. operation_creator — это функция. Откуда нам знать? За ним сразу следует открывающий парантез ( материал между ним и закрывающим парантезом ) говорит нам о количестве и типах параметров, которые принимает эта функция. Есть только аргумент типа int .
  3. То, что остается, это тип возвращаемого значения. Мысленно удалите operation_creator(int) с картинки, и у вас останется double (^) (double, double) . Это просто тип блока, который принимает два double значения и возвращает double . Итак, то, что возвращает наша функция operation_creator является блоком этого типа. Опять же, запомните, что если тип возвращаемого значения является блоком, то идентификатор «заперт» в середине.

Давайте вернемся к другой практической проблеме: напишите объявление функции с именем blah которая принимает в качестве единственного параметра блок без параметров и не возвращает значения, а также возвращает блок того же типа.

1
void(^blah(void(^)(void)))(void);

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

Хорошо, теперь вы можете анализировать или создавать сложные объявления блоков и функций, но все эти скобки и void , вероятно, заставляют ваши глаза слезиться! Непосредственное написание (и осмысление) этих сложных типов и объявлений является громоздким, подверженным ошибкам и включает в себя больше ментальных накладных расходов, чем следовало бы!

Пришло время поговорить об использовании оператора typedef , который (как вы, наверное, знаете) является конструкцией C, которая позволяет скрывать сложные типы за именем! Например:

1
typedef int ** IntPtrPtr;

Дает имя IntPtrPtr типу «указатель на указатель на int ». Теперь вы можете заменить int ** любом месте кода (например, объявление, приведение типа и т. Д.) С помощью IntPtrPtr .

Давайте определим имя типа для типа блока int(^)(int, int) . Вы можете сделать это так:

1
typedef int (^BlockTypeThatTakesTwoIntsAndReturnsInt) (int, int);

Это говорит о том, что BlockTypeThatTakesTwoIntsAndReturnsInt эквивалентен int(^)(int, int) , то есть блоку типа, который принимает два значения типа int и возвращает значение типа int .

Опять же, обратите внимание, что идентификатор (BlockTypeThatTakesTwoIntsAndReturnsInt) в приведенном выше операторе, который представляет имя, которое мы хотим дать типу, который мы определяем, заключен в детали типа typedef ‘d.

Как мы применим эту идею к blah функции, которую мы только что объявили? Тип параметра и возвращаемого типа один и тот же: void(^)(void) , поэтому давайте определим это следующим образом:

1
typedef void(^VVBlockType)(void);

Теперь перепишите заявление blah просто как:

1
VVBlockType blah(VVBlockType);

Там вы идете, гораздо приятнее! Правильно?

На этом этапе очень важно, чтобы вы могли различать следующие два утверждения:

1
2
typedef double (^BlockType)(double);
double(^blkptr)(double);

Они выглядят одинаково, за исключением ключевого слова typedef , но они имеют в виду совершенно разные вещи.

С помощью (2) вы объявили переменную указателя блока, которая может указывать на блоки типа double(^)(double) .

С помощью (1) вы определили тип по имени BlockType который может обозначать тип double (^)double . Итак, после (2) вы можете сделать что-то вроде: blkptr = ^(double x){ return 2 * x; }; blkptr = ^(double x){ return 2 * x; }; , И после (1) вы могли бы написать (2) как BlockType blkptr; (!)

Не продолжайте, если вы не поняли это различие совершенно!

Давайте вернемся к нашей функции работы. Можете ли вы написать определение для этого? Давайте напечатаем определение типа блока:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
typedef double(^BinaryOpBlock_t)(double, double);
 
BinaryOpBlock_t operation_creator(int op)
{
   if (op == 0)
      return ^(double x, double y) { return x + y;
   if (op == 1)
      return ^(double x, double y) { return x * y;
// … etc.
}
 
int main()
{
   BinaryOpBlock_t sum = operation_creator(0);
   NSLog(@»sum of 5.5 and 1.3 is %f», sum(5.5, 1.3));
   NSLog(@»product of 3.3 and 1.0 is %f», operation_creator(1)(3.3, 1.0));
}
Для более чистой реализации вы, вероятно, захотите определить перечисление (тип enum ) для представления целого числа параметров, которое принимает наша функция.

Другой пример. Определите функцию, которая принимает массив целых чисел, целое число, представляющее размер массива, и блок, который принимает целое число и возвращает целое число. Работа функции будет заключаться в том, чтобы применить блок (который мы представляем как математическую формулу) к каждому значению в массиве.

Мы хотим, чтобы тип нашего блока был int(^)(int) . Давайте typedef и определим нашу функцию:

1
2
3
4
5
6
7
8
9
typedef int(^iiblock_t)(int);
 
void func(int arr[], int size, iiblock_t formula)
{
   for ( int i = 0; i < size; i++ )
   {
      arr[i] = formula(arr[i]);
   }
}

Давайте использовать это в программе:

1
2
3
4
5
6
int main()
{
   int a[] = {10, 20, 30, 40, 50, 60};
   func(a, 6, ^(int x) { return x * 2; });
 
}

Разве это не круто? Мы смогли выразить нашу математическую формулу именно там, где нам было нужно!

Добавьте следующие операторы после вызова функции в предыдущем коде:

1
2
3
4
// place the following lines after func(a, 6, ^(int x) { return x * 2; });
   int n = 10;
   func(a, 6, ^(int x) { return x — n; } );
} // closing braces of main

Вы видели, что мы там делали? Мы использовали переменную n которая была в лексической области видимости нашего блока в теле литерала блока! Это еще одна чрезвычайно полезная особенность блоков (хотя в этом тривиальном примере может быть не совсем понятно, как, но давайте отложим это обсуждение).

Блок может «захватывать» переменные, которые появляются в лексической области оператора, вызывающего блок. На самом деле это захват только для чтения, поэтому мы не могли изменить n в теле блока. Значение переменной фактически копируется блоком во время его создания. Фактически это означает, что если бы мы изменили эту переменную в какой-то момент после создания литерала блока, но до его вызова, тогда блок все равно будет использовать «исходное» значение переменной, то есть значение, хранящееся в переменная во время создания блока. Вот простой пример того, что я имею в виду, основываясь на предыдущем коде:

1
2
3
4
5
int n = 5;
iiblock_t b = ^(int r) { return r * n;
// .. stuff
n = 1000;
func(a, 6, b);

Итак, как эта способность блоков полезна? В терминах высокого уровня, это позволяет нам использовать «контекстную информацию» во встроенном блоке из области, где блок определен. Возможно, не имеет смысла передавать эту информацию в качестве параметра в блок, так как это важно только в определенных контекстах, но в этих конкретных ситуациях мы действительно хотим использовать эту информацию в реализации нашего блока.

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

01
02
03
04
05
06
07
08
09
10
11
-(NSArray *) sortCountriesList:(NSArray *)listOfCountries
           withComparisonBlock: BOOL(^cmp) (Country *country1, Country *country2)
{
    // Implementation of some sorting algorithm that will make several passes through listOfCountries
    // whatever the algorithm, it will perform several comparisons in each pass and do something based on the result of the comparison
    
    BOOL isGreater = comp(countryA, countryB);
     
    if (isGreater) // do something, such as swapping the countries in the array
    // …
}

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

Опять же, вы должны понимать, что способность «подключать код» к нашему методу в форме блока сравнения дает нам возможность сортировать страны по некоторой формуле, которую мы можем указать на месте. Все хорошо. Теперь предположим, что вы понимаете, что было бы замечательно, если бы мы могли также ранжировать страны от самого низкого ранга до самого высокого. Вот как вы могли бы добиться этого с помощью блоков.

01
02
03
04
05
06
07
08
09
10
// Inside the body of some method belonging to the same class as the previous method
bool sortInAscendingOrder = YES;
// calling our sorting method:
NSArray *sortedList = [self sortCountriesList:list withComparisonBlock:^(Country *c1, Country *c2) {
                         if (c1.gdp > c2.gdp ) // comparing gdp for instance
                         {
                            if (sortInAscendingOrder) return YES;
                            else return NO;
                         }
}];

Вот и все! Мы использовали флаг sortInAscendingOrder который sortInAscendingOrder контекстную информацию о том, как мы хотели выполнить сортировку. Поскольку эта переменная входила в лексическую область объявления блока, мы смогли использовать ее значение в блоке и не должны беспокоиться об изменении значения до завершения блока. Нам не нужно было трогать наш -sortCountriesList: withComparisonBlock: чтобы добавить к нему параметр bool , или вообще не трогать его реализацию! Если бы мы использовали обычные функции, мы бы писали и переписывали код повсюду!

Давайте закончим этот урок, применяя все, что мы здесь узнали, с помощью классного iOS-блока из блоков!

Если вам когда-либо приходилось делать какие-либо пользовательские рисунки в объекте UIView, вы знаете, что вам нужно создать его подкласс и переопределить его drawRect: метод, в котором вы пишете код для рисования. Вы, вероятно, сочтете это рутинной работой, создайте целый подкласс, даже если все, что вы хотите сделать, это нарисовать простую линию, плюс необходимость изучить реализацию drawRect: всякий раз, когда вам нужно напомнить о том, что рисует представление. Какая скука!

Почему мы не можем сделать что-то подобное вместо этого:

1
[view drawWithBlock:^(/* parameters list */){ // drawing code for a line, or whatever }];

Ну с блоками можно! Здесь view является экземпляром нашего пользовательского подкласса UIView наделенного силой рисования с блоками. Обратите внимание, что, хотя нам все еще приходится UIView подкласс UIView , мы должны сделать это только один раз!

Давайте сначала спланируем заранее. Мы будем называть наш подкласс BDView (BD для «рисования блоков»). Поскольку рисование происходит в drawRect: мы хотим , чтобы BDView : BDView наш блок рисования! , Две вещи для размышления: (1) как BDView удерживает блок? (2) Какой будет тип блока?

Помните, я упоминал в начале, что блоки также похожи на объекты? Ну, это означает, что вы можете объявить свойства блока!

1
@property (nonatomic, copy) drawingblock_t drawingBlock;

Мы не определили, каким должен быть тип блока, но после того, как мы это сделаем, мы drawingblock_t его как drawingblock_t !

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

Как насчет типа блока? Напомним, что метод drawRect: принимает параметр CGRect который определяет область для рисования, поэтому наш блок должен принять это как параметр. Что еще? Что ж, рисование требует наличия графического контекста, и один из них доступен нам в drawRect: как «текущий графический контекст». Если мы рисуем с классами UIBezierPath , такими как UIBezierPath , то они неявно рисуют в текущем графическом контексте. Но если мы решим рисовать с помощью Core Graphics API на основе C, то нам нужно передать ссылки на графический контекст в функции рисования. Следовательно, чтобы обеспечить гибкость и позволить вызывающей стороне использовать функции Core Graphics, наш блок должен также принимать параметр типа CGContextRef который в BDView drawRect: BDView мы передаем в текущем графическом контексте.

Мы не заинтересованы в возврате чего-либо из нашего блока. Все, что он делает, это рисует. Следовательно, мы можем typedef тип нашего чертежного блока следующим образом:

1
typedef void (^drawingblock_t)(CGContextRef, CGRect);

Теперь в нашем -drawWithBlock: методе мы установим наше свойство drawingBlock в переданный блок и UIView метод setNeedsDisplay который вызовет drawRect: В drawRect: мы будем вызывать наш блок рисования, передавая ему текущий графический контекст (возвращаемый UIGraphicsGetCurrentContext() ) и прямоугольник рисования в качестве параметров. Вот полная реализация:

01
02
03
04
05
06
07
08
09
10
//BDView.h
#import <UIKit/UIKit.h>
 
typedef void (^drawingblock_t)(CGContextRef, CGRect);
 
@interface BDView : UIView
 
— (void)drawWithBlock:(drawingblock_t) blk;
 
@end
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// BDView.m
#import «BDView.h»
 
@interface BDView ()
 
@property (nonatomic, copy) drawingblock_t drawingBlock;
 
@end
 
@implementation BDView
 
— (void)drawWithBlock:(drawingblock_t)blk
{
    self.drawingBlock = blk;
    [self setNeedsDisplay];
}
 
 
— (void)drawRect:(CGRect)rect
{
    if (self.drawingBlock)
    {
        self.drawingBlock(UIGraphicsGetCurrentContext(), rect);
    }
}
 
@end

Таким образом, если бы у нас был контроллер view свойство view которого было экземпляром BDView , мы могли бы вызвать следующий метод из (скажем) viewDidLoad , чтобы нарисовать линию, которая проходила по диагонали через экран от верхнего левого угла до нижнего правого угла:

01
02
03
04
05
06
07
08
09
10
11
— (void)viewDidLoad
{
    [super viewDidLoad];
     
    BDView *view = (BDView *)self.view;
    [view drawWithBlock:^(CGContextRef context, CGRect rect){
        CGContextMoveToPoint(context, rect.origin.x, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
        CGContextStrokePath(context);
    }];
}

Brilliant!


В этом вводном уроке по блокам мы по общему признанию пропустили несколько вещей. Я упомяну пару из них здесь, так что вы можете посмотреть их в более продвинутых статьях и ссылках:

  • Мы не много говорили о семантике памяти блоков. Хотя автоматический подсчет ссылок освобождает нас от большой нагрузки в этом отношении, в определенных сценариях требуется больше понимания.
  • Спецификатор, __blockкоторый позволяет блоку изменять переменную в своей лексической области видимости.

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

Я также рекомендую вам взглянуть на BlocksKit , который включает в себя множество интересных утилит блоков, которые могут сделать вашу жизнь как разработчика iOS проще. Удачного кодирования!