Надеюсь, что пост будет полезен людям которые знакомы с лямбдами 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 проекты