Надеюсь, что пост будет полезен людям которые знакомы с лямбдами C++, но хотят изучить блоки Objective-C и наоборот.
Здесь я постарался описать синтаксис замыканий, механизмы захвата контекста, управление памятью и взаимодествие лямбд и блоков между собой.
Во всех примерах использовался Apple LLVM Compiler 4.2 (Clang). Для приведенного Obj-C кода не используется ARC, т.к я придерживаюсь мнения, что необходимо знать как работает non-ARC код, чтобы понять как работает ARC.
Блоки в Objective-C — это реализация замыканий [2]. Блоки представляют собой анонимные функции, которые могут захватывать контекст (текущие стековое переменные и переменные-члены классов). Блоки во время выполнения представляются объектами, они являются аналогами лямбда выражений в C++.
Лямбды в C++ — тоже являются реализацией замыканий и представляют собой безымянные локальные функции.
[3]
Блоки имеют семантику указателя.
Блоки в Objective-C уже прочно нашли свое применение как в стандартных фреймворках (Foundation, UIKit) так и в сторонних библиотеках (AFNetworking, BlocksKit).
Приведем пример в виде категории класса NSArray
В первую очередь они отлично подходят для асинхронных операций, в этом можно убедиться используя AFNetworking, да и работать с ними в GCD — одно удовольствие.
Мы можем определять свои типы блоков, например:
Тот же самый код в виде лямбды
[11]
Приведем аналогичный пример выделения подмножества для лямбды
Мы можем автоматически использовать значения стековых переменных в блоках, если не изменяем их. Например, в приведенном выше примере, мы не указывали в объявлении блока, что мы хотим захватить переменную
Мы просто брали ее значение по имени объявленном в теле функции. Если бы мы захотели изменить ее значение в теле блока, нам пришлось бы пометить переменную модификатором
Для того чтобы отправлять сообщения объекту, указатель на который мы передаем в блок, необходимости помечать указатель как
Но иногда, все же, необходимо пометить указатель на объект с помощью
Захваченные переменные указываются в конкретном месте [5], а именно внутри квадратных скобок [ ]
К захвату контекста имеет отношение спецификатор
что вы можете изменять копии переменных переданных по значению. Подробнее в следующем разделе.
Блоки — это объекты, создаются они на стеке (в последситвие они могут быть перенесены в кучу (heap))
Блоки могут существовать в виде 3-х имплементаций [7].
Приведем пример цикла владения:
Допустим вы захотели создать класс который совершает
А потом вы захотели создать конкретный запрос для конкретного API вашего сервера
Ошибка в этой строчке

Избежать этого можно было создав на стеке промежуточный указатель на self с модификатором __block, вот так:
Или же, можно было перенести блоки из сигнатуры метода инициализации в метод
Приведем другой пример неправильного memory management с блоками, пример взят из видео лекции [7]
Если бы мы скопировали блок в кучу (heap) и передали вверх по стеку, ошибки бы не произошло.
Также данный пример не вызовет ошибки в ARC коде.
Реализация лямбд в runtime, может быть специфична в разных компиляторах. Говорят, что управление памятью для лямбд не очень описано в стандарте.[9]
Рассмотрим распространенную имплементацию.
Лямбды в C++ — это объекты неизвестного типа, создающиеся на стеке.
Лямбды которые не захватывают никакого контекста, могут быть приведены к указателю на функцию, но все же это не означает, что они сами являются просто указателями на функцию. Лямбда — это обычный объект, с конструктором и деструктором, выделяющийся на стеке.
Приведем несколько примеров перемещения лямбды в heap
Теперь можно передать функцию в переменную-член какого-нибудь объекта.
mutable после объявления аргументов лямбды говорит о том, что вы можете изменять значения копий переменных, захваченных по значению (Значение оригинальной переменной изменяться не будет). Например, если бы мы определили лямбду так:
Так как Objecitve-C++ сочетает в себе как Objective-C так и C++, в нем можно одновременно использовать лямбды и блоки. Как же лямбды и блоки относятся друг к другу?
Следует сказать, что операции между лямбдами и блоками достаточно экзотичны, я например ни разу не встречал подобные присвоения в проектах.
Здесь я постарался описать синтаксис замыканий, механизмы захвата контекста, управление памятью и взаимодествие лямбд и блоков между собой.
Во всех примерах использовался Apple LLVM Compiler 4.2 (Clang). Для приведенного Obj-C кода не используется ARC, т.к я придерживаюсь мнения, что необходимо знать как работает non-ARC код, чтобы понять как работает ARC.
Разделы:
Блоки в Objective-C — это реализация замыканий [2]. Блоки представляют собой анонимные функции, которые могут захватывать контекст (текущие стековое переменные и переменные-члены классов). Блоки во время выполнения представляются объектами, они являются аналогами лямбда выражений в C++.
Лямбды в C++ — тоже являются реализацией замыканий и представляют собой безымянные локальные функции.
Синтаксис
Obj-C блоки
[3]В текстовом виде
int multiplier = 7;
int (^myBlock)(int) = ^(int num) { return num * multiplier;};
NSLog(@”%d”, myBlock(4)); // выведет 28
- ^ — этот символ говорит компилятору о том что переменая — блок
- int — блок принимает один параметр типа int, и возвращает параметр типа int
- multiplier — переменная которая приходит к нам из контекста (об этом более подробно в разделе “Захват контекста”)
Блоки имеют семантику указателя.
Блоки в Objective-C уже прочно нашли свое применение как в стандартных фреймворках (Foundation, UIKit) так и в сторонних библиотеках (AFNetworking, BlocksKit).
Приведем пример в виде категории класса NSArray
Пример использования блока в NSArray
// имплементация категории
@implementation NSArray (Blocks)
// метод возвращает массив, элементы которого соответствуют предикату
- (NSArray*)subarrayWithPredicate:(BOOL(^)(id object, NSUInteger idx, BOOL *stop))predicte {
NSMutableArray *resultArray = [NSMutableArray array];
BOOL shouldStop = NO;
for (id object in self) {
if (predicte(object, [self indexOfObjectIdenticalTo:object], &shouldStop)) {
[resultArray addObject:object];
}
if (shouldStop) {
break;
}
}
return [[resultArray copy] autorelease];
}
@end
// где-то в клиентском коде
NSArray *numbers = @[@(5), @(3), @(8), @(9), @(2)];
NSUInteger divisor = 3;
NSArray *divisibleArray = [numbers subarrayWithPredicate:^BOOL(id object, NSUInteger idx, BOOL *stop) {
BOOL shouldAdd = NO;
// нам нужны числа кратные 3
NSAssert([object isKindOfClass:[NSNumber class]], @"object != number");
// обратим внимание, что переменную divisor мы взяли из контекста
if ([(NSNumber *)object intValue] % divisor == 0) {
shouldAdd = YES;
}
return shouldAdd;
}];
NSLog(@"%@", divisibleArray); // выведет 3, 9
В первую очередь они отлично подходят для асинхронных операций, в этом можно убедиться используя AFNetworking, да и работать с ними в GCD — одно удовольствие.
Мы можем определять свои типы блоков, например:
Объявление типа блока
typedef int (^MyBlockType)(int number, id object);
С++ лямбды
Тот же самый код в виде лямбды
[11]В текстовом виде
int multiplier = 7;
auto lambda = [&multiplier](int num) throw() -> int
{
return multiplier * num;
};
lambda(4); // равно 28
[]— начало объявления лямбды, внутри — захват контекста&multiplier— захваченная переменная (&означает что захвачена по ссылке)int— входной параметрmutable— ключевое слово которое позволяет модифицировать переменные захваченные по значениюthrow()— обозначает что лямбда не выбрасывает никаких исключений наружу
Приведем аналогичный пример выделения подмножества для лямбды
Извлечение подмножества из коллекции по предикату
template<class InputCollection, class UnaryPredicate>
void subset(InputCollection& inputCollection, InputCollection& outputCollection, UnaryPredicate predicate)
{
typename InputCollection::iterator iterator = inputCollection.begin();
for (;iterator != inputCollection.end(); ++iterator) {
if (predicate(*iterator)) {
outputCollection.push_back(*iterator);
}
}
return;
}
int main(int argc, const char * argv[])
{
int divisor = 3;
std::vector<int> inputVector = {5, 3, 8, 9, 2};
std::vector<int> outputVector;
subset(inputVector, outputVector, [divisor](int number){return number % divisor == 0;});
// выводим значения полученной коллекции
std::for_each( outputVector.begin(),
outputVector.end(),
[](const int& number){std::cout << number << std::endl;} );
}
Захват контекста
Obj-C блоки
Мы можем автоматически использовать значения стековых переменных в блоках, если не изменяем их. Например, в приведенном выше примере, мы не указывали в объявлении блока, что мы хотим захватить переменную
multiplier (в отличии от лямбды, в лямбде мы могли бы указать [&] чтобы захватить весь контекст по ссылкам, или [=] чтобы захватить весь контекст по значению).Мы просто брали ее значение по имени объявленном в теле функции. Если бы мы захотели изменить ее значение в теле блока, нам пришлось бы пометить переменную модификатором
__blockПример изменения значения переменной из контекста
__block int first = 7;
void (^myBlock2)(int) = ^(int second) { first += second;};
myBlock2(4);
NSLog(@"%d", first); // выведет 11
Для того чтобы отправлять сообщения объекту, указатель на который мы передаем в блок, необходимости помечать указатель как
__block нет. Ведь по сути, когда мы отправляем сообщение объекту — мы не изменяем его указатель. Пример отправки сообщения объекту из контекста
NSMutableArray *array = [NSMutableArray array];
void (^myBlock3)() = ^() { [array addObject:@"someString"];};
myBlock3(); // добавит someString в array
Но иногда, все же, необходимо пометить указатель на объект с помощью
__block, чтобы избежать утечек памяти. (Об этом подробнее в разделе “Управление памятью”) С++ лямбды
Захваченные переменные указываются в конкретном месте [5], а именно внутри квадратных скобок [ ]
[&]— означает что мы захватываем все символы по ссылке[=]— все символы по значению[a, &b]—aзахвачена по значению,bзахвачена по ссылке[]— ничего не захвачено
К захвату контекста имеет отношение спецификатор
mutable, он говорит о том,что вы можете изменять копии переменных переданных по значению. Подробнее в следующем разделе.
Управление памятью
Obj-C блоки
Блоки — это объекты, создаются они на стеке (в последситвие они могут быть перенесены в кучу (heap))
Блоки могут существовать в виде 3-х имплементаций [7].
- Когда мы не используем переменные из контекста (из стека) внутри блока — создается
NSGlobalBlock, который реализован в виде синглтона. - Если мы используем контекстные переменные, то создается
NSStackBlock, который уже не является синглтоном, но распологается на стеке. - Если мы используем функцию
Block_copy, или хотим чтобы наш блок был сохранен внутри какого-то объекта размещенного в кучи, например как свойство объекта:@property (nonatomic, copy) MyBlockType myBlock;то создается объект классаNSMallocBlock, который захватывает и овладевает (овладевает == посылает сообщениеretain) объектами переданными в контексте. Это очень важное свойство, потому как может приводить к утечкам памяти, если с ним обращаться невнимательно. Блоки могут создавать циклы владения (retain cycle). Еще важно отметить, что если мы будем использовать значениеpropertyвNSMallocBlock— ретейниться будет не само свойство, а объект которому свойство принадлежит.
Приведем пример цикла владения:
Допустим вы захотели создать класс который совершает
HTTP запрос с асинхронным API PKHTTPReuquestРеализация PKHTTPReuquest
typedef void (^PKHTTPRequestCompletionSuccessBlock)(NSString *responseString);
typedef void (^PKHTTPRequestCompletionFailBlock)(NSError* error);
@protocol PKRequest <NSObject>
- (void)startRequest;
@end
@interface PKHTTPRequest : NSObject <PKRequest>
// designated initializer
- (id)initWithURL:(NSURL *)url
successBlock:(PKHTTPRequestCompletionSuccessBlock)success
failBlock:(PKHTTPRequestCompletionFailBlock)fail;
@end
@interface PKHTTPRequest () <NSURLConnectionDelegate>
@property (nonatomic, copy) PKHTTPRequestCompletionSuccessBlock succesBlock;
@property (nonatomic, copy) PKHTTPRequestCompletionFailBlock failBlock;
@property (nonatomic, retain) NSURL *url;
@property (nonatomic, retain) NSURLConnection *connection;
@property (nonatomic, retain) NSMutableData *data;
@end
@implementation PKHTTPRequest
#pragma mark - initialization / deallocation
// designated initializer
- (id)initWithURL:(NSURL *)url
successBlock:(PKHTTPRequestCompletionSuccessBlock)success
failBlock:(PKHTTPRequestCompletionFailBlock)fail {
self = [super init];
if (self != nil) {
self.succesBlock = success;
self.failBlock = fail;
self.url = url;
NSURLRequest *request = [NSURLRequest requestWithURL:self.url];
self.connection = [[[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:NO] autorelease];
}
return self;
}
- (id)init {
NSAssert(NO, @"Use desiganted initializer");
return nil;
}
- (void)dealloc {
self.succesBlock = nil;
self.failBlock = nil;
self.url = nil;
self.connection = nil;
self.data = nil;
[super dealloc];
}
#pragma mark - public methods
- (void)startRequest {
self.data = [NSMutableData data];
[self.connection start];
}
#pragma mark - NSURLConnectionDelegate implementation
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.data appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
self.failBlock(error);
self.data = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
self.succesBlock([NSString stringWithUTF8String:self.data.bytes]);
self.data = nil;
}
@end
А потом вы захотели создать конкретный запрос для конкретного API вашего сервера
PKGetUserNameRequest который работает с PKHTTPReuquestРеализация PKGetUserNameRequest
typedef void (^PKGetUserNameRequestCompletionSuccessBlock)(NSString *userName);
typedef void (^PKGetUserNameRequestCompletionFailBlock)(NSError* error);
@interface PKGetUserNameRequest : NSObject <PKRequest>
- (id)initWithUserID:(NSString *)userID
successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success
failBlock:(PKGetUserNameRequestCompletionFailBlock)fail;
@end
NSString *kApiHost = @"http://someApiHost.com";
NSString *kUserNameApiKey = @"username";
@interface PKGetUserNameRequest ()
@property (nonatomic, retain) PKHTTPRequest *httpRequest;
- (NSString *)parseResponse:(NSString *)response;
@end
@implementation PKGetUserNameRequest
#pragma mark - initialization / deallocation
- (id)initWithUserID:(NSString *)userID
successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success
failBlock:(PKGetUserNameRequestCompletionFailBlock)fail {
self = [super init];
if (self != nil) {
NSString *requestString = [kApiHost stringByAppendingFormat:@"?%@=%@", kUserNameApiKey, userID];
self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString]
successBlock:^(NSString *responseString) {
// роковая ошибка - обращение к self
NSString *userName = [self parseResponse:responseString];
success(userName);
} failBlock:^(NSError *error) {
fail(error);
} ] autorelease];
}
return self;
}
- (id)init {
NSAssert(NO, @"Use desiganted initializer");
return nil;
}
- (void)dealloc {
self.httpRequest = nil;
[super dealloc];
}
#pragma mark - public methods
- (void)startRequest {
[self.httpRequest startRequest];
}
#pragma mark - private methods
- (NSString *)parseResponse:(NSString *)response {
/* ...... */
return userName;
}
@end
Ошибка в этой строчке
NSString *userName = [self parseResponse:responseString]; — когда мы вызываем что-то у self в Malloc блоке, self ретейнится, образовался следующий цикл в графе владения:
Избежать этого можно было создав на стеке промежуточный указатель на self с модификатором __block, вот так:
Пример разрыва цикла владения
// разрываем цикл владения
__block PKGetUserNameRequest *selfRequest = self;
self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString]
successBlock:^(NSString *responseString) {
NSString *userName = [selfRequest parseResponse:responseString];
success(userName);
} failBlock:^(NSError *error) {
fail(error);
} ] autorelease];
Или же, можно было перенести блоки из сигнатуры метода инициализации в метод
startRequest,startRequestwithCompaltion:fail:, и ретейнить блоки только на время выполнения запроса. Тогда можно было бы обойтись без модификатора __block. Это решало бы еще одну проблему: в приведенном выше примере существует опасность, что к моменту вызова блока (к моменту завершения запроса) объект типа PKGetUserNameRequest уже перестанет существовать (будет вызван метод dealloc), так как блок обеспечивает слабую связь. И по указателю selfRequest будет висеть зомби, что вызовет crash.Приведем другой пример неправильного memory management с блоками, пример взят из видео лекции [7]
Ошибка с NSStackBlock
void addBlockToArray(NSMutableArray* array) {
NSString *string = @"example string";
[array addObject:^{
printf("%@\n", string);
}];
}
void example() {
NSMutableArray *array = [NSMutableArray array];
addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
Если бы мы скопировали блок в кучу (heap) и передали вверх по стеку, ошибки бы не произошло.
Также данный пример не вызовет ошибки в ARC коде.
С++ лямбды
Реализация лямбд в runtime, может быть специфична в разных компиляторах. Говорят, что управление памятью для лямбд не очень описано в стандарте.[9]
Рассмотрим распространенную имплементацию.
Лямбды в C++ — это объекты неизвестного типа, создающиеся на стеке.
Лямбды которые не захватывают никакого контекста, могут быть приведены к указателю на функцию, но все же это не означает, что они сами являются просто указателями на функцию. Лямбда — это обычный объект, с конструктором и деструктором, выделяющийся на стеке.
Приведем несколько примеров перемещения лямбды в heap
Пример перемещения лямбды в heap
// Способ №1
auto lamb = []() {return 5;};
auto func_lamb_ptr = new std::function<int()>(lamb);
// Способ №2:
auto lamb = []() {return 5;};
auto* p = new decltype(lamb)(lamb);
// Способ №3:
template <typename T>
T* heap_alloc(T const& value)
{
return new T(value);
}
auto* p = heap_alloc([]() {return 5;});
// Способ №4:
std::vector<decltype(lamb)> v;
v.push_back(lamb);
Теперь можно передать функцию в переменную-член какого-нибудь объекта.
mutable после объявления аргументов лямбды говорит о том, что вы можете изменять значения копий переменных, захваченных по значению (Значение оригинальной переменной изменяться не будет). Например, если бы мы определили лямбду так:
auto lambda = [multiplier](int num) throw() mutable то могли бы изменять значение multiplier внутри лямбды, но multipler объявленный в функции не изменился. Более того, измененное значение multiplier сохраняется от вызова к вызову данного экземпляра лямбды. Можно представить это так: в экземпляре лямбды (в объекте) создаются переменные члены соответствующие переданным параметрам. Тут нужно быть осторожнее, потому что если мы скопируем экземпляр лямбды и вызовем ее, то эти переменные-члены не изменятся в оригинальной лямбде, они изменятся только в скопированной. Иногда нужно передавать лямбды обернутыми в std::ref. Obj-C блоки не предаставляют такой возможности «из коробки». Objective-C++
Так как Objecitve-C++ сочетает в себе как Objective-C так и C++, в нем можно одновременно использовать лямбды и блоки. Как же лямбды и блоки относятся друг к другу?
- Мы можем присвоить блоку лямбду.
Примерvoid (^block_example)(int); auto lambda_example = [](int number){number++; NSLog(@"%d", number);}; block_example = lambda_example; block_example(10); // log 11
- Мы можем присвоить блок объетку std::function
Здесь стоит отметить что Objective-C и C++ имеют разные политики управления памятью, и хранение блока вstd::functionможет приводить к «висячим» ссылкам.
- Мы не можем присвоить лямбде блок.
У лямбды не определен оператор copy-assignment. Поэтому мы не можем присвоить ей ни блок ни даже саму себя.
Ошибка присвоенияint main() { auto lambda1 = []() -> void { printf("Lambda 1!\n"); }; lambda1 = lambda1; // error: use of deleted function ‘main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&)’ return 0; }
Следует сказать, что операции между лямбдами и блоками достаточно экзотичны, я например ни разу не встречал подобные присвоения в проектах.
Ссылки по теме
- О лямбдах C++
- Замыкания
- О блоках Apple
- Сравнение лямбд и блоков на англ
- Доки C++ лямбд
- О блоках
- Отличное видео о блоках
- Вопрос об организации памяти C++ лямбд на stackoverflow.com
- Вопрос про имплементацию C++ лямбд в runtime
- О взаимодействии лямбд и блоков
- Синтаксис лямбд
- О взаимодействии Objective-C и C++
- Способы встраивания C++ в Objective-C проекты
