Здесь я постарался описать синтаксис замыканий, механизмы захвата контекста, управление памятью и взаимодествие лямбд и блоков между собой.
Во всех примерах использовался 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/
Добавить комментарий