Здесь я постарался описать синтаксис замыканий, механизмы захвата контекста, управление памятью и взаимодествие лямбд и блоков между собой.
Во всех примерах использовался Apple LLVM Compiler 4.2 (Clang). Для приведенного Obj-C кода не используется ARC, т.к я придерживаюсь мнения, что необходимо знать как работает non-ARC код, чтобы понять как работает ARC.
Разделы:
Блоки в Objective-C — это реализация замыканий [2]. Блоки представляют собой анонимные функции, которые могут захватывать контекст (текущие стековое переменные и переменные-члены классов). Блоки во время выполнения представляются объектами, они являются аналогами лямбда выражений в C++.
Лямбды в C++ — тоже являются реализацией замыканий и представляют собой безымянные локальные функции.
Синтаксис
Obj-C блоки
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
// имплементация категории @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) объектами переданными в контексте. Это очень важное свойство, потому как может приводить к утечкам памяти, если с ним обращаться невнимательно. Блоки могут создавать циклы владения. Еще важно отметить, что если мы будем использовать значениеpropertyвNSMallocBlock— ретейниться будет не само свойство, а объект которому свойство принадлежит.
Приведем пример цикла владения:
Допустим вы захотели создать класс который совершает HTTP запрос с асинхронным API 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
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:, и ретейнить блоки только на время выполнения запроса.
Приведем другой пример неправильного memory management с блоками, пример взят из видео лекции [7]
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 является приведение ее к типу std::function
auto lamb = []() {return 5;}; auto func_lamb_ptr = new std::function<int()>(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 проекты
ссылка на оригинал статьи http://habrahabr.ru/post/204356/
Добавить комментарий