Objective-c блоки и c++ лямбды

от автора

Надеюсь, что пост будет полезен людям которые знакомы с лямбдами C++, но хотят изучить блоки Objective-C и наоборот.
Здесь я постарался описать синтаксис замыканий, механизмы захвата контекста, управление памятью и взаимодествие лямбд и блоков между собой.
Во всех примерах использовался Apple LLVM Compiler 4.2 (Clang). Для приведенного Obj-C кода не используется ARC, т.к я придерживаюсь мнения, что необходимо знать как работает non-ARC код, чтобы понять как работает ARC.

Разделы:

  1. Синтаксис
  2. Захват контекста
  3. Управление памятью
  4. Objective-C++

Блоки в 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].

  1. Когда мы не используем переменные из контекста (из стека) внутри блока — создается NSGlobalBlock, который реализован в виде синглтона.
  2. Если мы используем контекстные переменные, то создается NSStackBlock, который уже не является синглтоном, но распологается на стеке.
  3. Если мы используем функцию Block_copy, или хотим чтобы наш блок был сохранен внутри какого-то объекта размещенного в кучи, например как свойство объекта: @property (nonatomic, copy) MyBlockType myBlock; то создается объект класса NSMallocBlock, который захватывает и овладевает (овладевает == посылает сообщение retain) объектами переданными в контексте. Это очень важное свойство, потому как может приводить к утечкам памяти, если с ним обращаться невнимательно. Блоки могут создавать циклы владения. Еще важно отметить, что если мы будем использовать значение 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:, и ретейнить блоки только на время выполнения запроса.

Приведем другой пример неправильного 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 является приведение ее к типу std::function

Пример перемещения лямбды в heap

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++, в нем можно одновременно использовать лямбды и блоки. Как же лямбды и блоки относятся друг к другу?

  1. Мы можем присвоить блоку лямбду.
    Пример

        void (^block_example)(int);     auto lambda_example = [](int number){number++; NSLog(@"%d", number);};     block_example = lambda_example;     block_example(10); // log 11 

  2. Мы можем присвоить блок объетку std::function

    Здесь стоит отметить что Objective-C и C++ имеют разные политики управления памятью, и хранение блока в std::function может приводить к «висячим» ссылкам.

  3. Мы не можем присвоить лямбде блок.

    У лямбды не определен оператор 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; } 

Следует сказать, что операции между лямбдами и блоками достаточно экзотичны, я например ни разу не встречал подобные присвоения в проектах.

Ссылки по теме

  1. О лямбдах C++
  2. Замыкания
  3. О блоках Apple
  4. Сравнение лямбд и блоков на англ
  5. Доки C++ лямбд
  6. О блоках
  7. Отличное видео о блоках
  8. Вопрос об организации памяти C++ лямбд на stackoverflow.com
  9. Вопрос про имплементацию C++ лямбд в runtime
  10. О взаимодействии лямбд и блоков
  11. Синтаксис лямбд
  12. О взаимодействии Objective-C и C++
  13. Способы встраивания C++ в Objective-C проекты

ссылка на оригинал статьи http://habrahabr.ru/post/204356/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *