Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C Blocks, How to preserve context values?

here's a tricky issue that I'm just not able to crack down.

I understand that Obj-C blocks are not closures per se and their implementation is somehow different than Javascript closures but I'll still use a Javascript example in order to show what I'm trying to accomplish (people familiar with Javascript will get it).

On Javascript you could create a 'function factory' like the one below:

//EXAMPLE A
var _arr = [], i = 0;
for(;i<8;++i) {
  _arr[i] = function() {
    console.log('Result:' + i);
  };
}
//BY THE END OF THIS LOOP i == 7
_arr[0]();
_arr[1]();
_arr[2]();
...
_arr[7]();

Which fills an array named _arr with their corresponding function and then evaluates all of them. Notice that the result for the above code will output...

Result: 7
Result: 7
Result: 7
...
Result: 7

... '7' in all of the functions, which is correct because by the time that the functions get evaluated the value of i equals 8, even though the value of i is 0...7 while they are created, here we conclude that i is passed by reference and not by value.

If we would like to 'fix' this and have each function to use the value of i at the moment is created, we would write something like this instead:

//EXAMPLE B
var _arr = [], i = 0;
for(;i<8;++i) {
  _arr[i] = (function(new_i){
    return function() {
      console.log(new_i);
    };
  })(i); //<--- HERE WE EVALUATE THE FUNCTION EACH TIME THE LOOP ITERATES, SO THAT EVERYTHING INSIDE OF THIS 'RETAINS' THE VALUES 'AT THAT MOMENT'
}
//BY THE END OF THIS LOOP i == 7, BUT IT DOESN'T MATTER ANYMORE
_arr[0]();
_arr[1]();
_arr[2]();
...
_arr[7]();

Which instead of creating the final function directly, makes use of an intermediate closure which returns the final function with the correct values 'fixed' inside of it; and consequently will return:

Result: 0
Result: 1
Result: 2
...
Result: 7

Now...

I'm trying to do the same thing by using Objective-C blocks.

Here's my code for Example A (in Obj-C):

NSMutableArray *_arr = [NSMutableArray arrayWithCapacity:0];
int i = 0;
for(;i<8;++i) {
    [_arr addObject:^{
        NSLog(@"Result: %i", i);
    }];
}
//BY THE END OF THIS LOOP i == 7
[_arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    ((void (^)())obj)();
}];

And this will output...

Result: 7
Result: 7
...
Result: 7

... which is also correct because the function actually holds a reference to i.

The question is, how should I rewrite that loop above in order to emulate the behavior shown on Example B? (i retaining the value it had at the moment of the function creation)

I've tried by writing the loop like this:

for(;i<8;++i) {
    [_arr addObject:^(int new_i){
        return ^{
            NSLog(@"Result: %i", new_i);
        };
    }(i)];
}

But it gives the following error when compiling: Returning block that lives on the local stack

Thanks and best ;D!

like image 225
Ale Morales Avatar asked Aug 21 '12 05:08

Ale Morales


2 Answers

You are incorrect in saying that Objective-C blocks capture by reference. They actually capture by value. (except with __block variables, which we will not go into here.) You can verify that here:

int x = 42;
void (^foo)() = ^ { NSLog(@"%d", x); };
x = 17;
foo(); // logs "42"

The problem you are having is that blocks start out on the stack, and stack-blocks are only valid for the local scope of the block expression. In this case your block expression is inside a for loop. That means that block object is no longer valid after the end of the iteration of the for loop. But you put a pointer to this block object into the array.

Like with local variables inside the for loop, that memory location in the stack frame is then re-used (it happens to in this case, but it is up to the compiler) for the stack block on the next iteration of the loop. So if you inspect the values stored in the array, you will find that all of the object pointers are equal. So instead of having 8 pointers to 8 block objects, you have 8 pointers to the same block object. That's why you think it is capturing it "by reference". But what really is happening is that the block on the stack is overwritten at every iteration, so your array contains multiple copies of the pointer to this location, so you are seeing this same block (the one created on the last iteration) over and over again.

The answer is that you need to copy the block before putting it into the array. A copied block is on the heap and has dynamic lifetime (memory managed like other objects).

NSMutableArray *_arr = [NSMutableArray arrayWithCapacity:0];
int i = 0;
for(;i<8;++i) {
    [_arr addObject:[[^{
        NSLog(@"Result: %i", i);
    } copy] autorelease]];
}
[_arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    ((void (^)())obj)();
}];

You don't need to wrap a second closure that is immediately executed to do this in Objective-C the way you do in JavaScript.

like image 118
newacct Avatar answered Oct 08 '22 20:10

newacct


If you want to return a block you need to copy it first, either by sending a copy message or using the Block_copy function. To keep from leaking memory you'll have to release the copied block later, for example using autorelease

for(;i<8;++i) {
    [_arr addObject:^(int new_i){
        return [[^{
            NSLog(@"Result: %i", new_i);
        } copy] autorelease];
    }(i)];
}
like image 35
Sven Avatar answered Oct 08 '22 20:10

Sven