Search code examples
objective-cobjective-c-blocks

Why objective-c's block cannot capture values dynamically?


I wanna implement defer in objective-c. And here's my code:

/**
  RAII : ABC->~ABC
 */
@interface DeferImpl_ : NSObject
/**
 *  init with a callback
 *
 *  @return
 */
-(instancetype) initWithCallback:(void(^)())callback;
/**
 *  a callback
 */
@property(nonatomic, copy) void(^callback)();

@end

/**
 *  Defer
 *
 *  @param X { statement; statement; ... }
 *
 *  @return
 */
#define DEFER(X)  [[DeferImpl_ alloc] initWithCallback:^X]

#define SAFE_INVOKE(x) do{if(x){(x)();}}while(0)
@implementation DeferImpl_
/**
 *  invoke callback
 */
-(void) dealloc {
    SAFE_INVOKE(self.callback);
}

-(instancetype) initWithCallback:(void(^)())callback {
    self = [super init];
    self.callback = callback;
    return self;
}

@end

The implementation is simple and seems easy to use. But it's buggy!.

Here comes what I feel frustrated.

int main(void)
{
    NSInteger count = 0;
    DEFER({
        NSLog(@"Defer: %@", @(count));
    });
    count = 123;
    NSLog(@"Before defer block!");
    return 0;
}

Log is:

2017-01-12 17:31:32.401 test[73724:18571479] Defer: 0

2017-01-12 17:31:32.402 test[73724:18571479] Before defer block!

So is there anyone can tell me why count still 0 in the block?


Solution

  • You have hidden most of your work into macros (not a good programming style!) and your are missing the basic point - DEFER calls the block immediately, before even reaching count = 123.

    If you don't assign the result of [DeferImpl_ alloc] to any variable, it will get released immediately, also immediately calling the block.

    Your current code is the same as writing directly:

    NSInteger count = 0;
    NSLog(@"Defer: %@", @(count));
    count = 123;
    NSLog(@"Before defer block!");
    

    If you change your code to:

    NSInteger count = 0;
    id x = DEFER({
        NSLog(@"Defer: %@", @(count));
    });
    count = 123;
    NSLog(@"Before defer block!");
    

    Your result will be:

    Before defer block!
    Defer: 0

    Now, why the value is still zero? Because blocks capture variables by value. To capture the reference, you will have to add __block:

    __block NSInteger count = 0;
    id x = DEFER({
        NSLog(@"Defer: %@", @(count));
    });
    count = 123;
    NSLog(@"Before defer block!");
    

    Before defer block!
    Defer: 123

    See Blocks and Variables