Pull to refresh

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

Reading time10 min
Views25K
Надеюсь, что пост будет полезен людям которые знакомы с лямбдами 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) объектами переданными в контексте. Это очень важное свойство, потому как может приводить к утечкам памяти, если с ним обращаться невнимательно. Блоки могут создавать циклы владения (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++, в нем можно одновременно использовать лямбды и блоки. Как же лямбды и блоки относятся друг к другу?

  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 проекты
Tags:
Hubs:
+22
Comments19

Articles