Objective-C Blocks,如何保留上下文值?

时间:2012-08-21 05:21:04

标签: objective-c ios closures objective-c-blocks block

这是一个棘手的问题,我无法打击。

我理解Obj-C块本身不是闭包,它们的实现与Javascript闭包有些不同但我仍然会使用Javascript示例来展示我想要完成的事情(熟悉Javascript的人会得到它。)

在Javascript上你可以像下面那样创建一个'功能工厂':

//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]();

使用相应的函数填充名为_arr的数组,然后计算所有这些数组。请注意,上面代码的结果将输出...

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

...在所有函数中都是'7',这是正确的,因为在评估函数的时候,i的值等于8,即使i的值是0 ... 7而它们被创建在这里我们得出结论,我是通过引用而不是通过值传递的。

如果我们想要“修复”这个并让每个函数在创建时使用i的值,我们会写这样的东西:

//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]();

而不是直接创建最终函数,而是使用一个中间闭包,它返回最终函数,其中包含正确的值'fixed';因此将返回:

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

现在...

我正在尝试使用Objective-C块做同样的事情。

这是我的示例A的代码(在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)();
}];

这将输出......

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

...这也是正确的,因为该函数实际上持有对i的引用。

问题是,我应该如何重写上面的循环以模拟示例B中显示的行为?(我保留了函数创建时的值)

我试过写这样的循环:

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

但是在编译时它会出现以下错误:返回生成在本地堆栈上的块

谢谢,最好; D!

2 个答案:

答案 0 :(得分:11)

您说Objective-C阻止通过引用捕获是不正确的。它们实际上是按价值捕获的(__block变量除外,我们不会在这里进行讨论。)您可以在此验证:

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

您遇到的问题是块从堆栈开始,堆栈块仅对块表达式的本地范围有效。在这种情况下,块表达式位于for循环中。这意味着在for循环的迭代结束后,块对象不再有效。但是你将一个指向这个块对象的指针放入数组中。

与for循环内部的局部变量一样,堆栈帧中的内存位置然后在下一次迭代时重复使用(在这种情况下,它适用于编译器)堆栈块。环。因此,如果检查存储在数组中的值,您会发现所有对象指针都是相同的。因此,不是有8个指向8个块对象的指针,而是指向同一个块对象的8个指针。这就是为什么你认为它是“通过引用”捕获它。但真正发生的是堆栈上的块在每次迭代时都会被覆盖,因此您的数组包含指向此位置的指针的多个副本,因此您会反复看到同一个块(在最后一次迭代中创建的块)试。

答案是你需要在将块放入数组之前复制它。复制的块位于堆上并具有动态生存期(内存管理与其他对象一样)。

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)();
}];

你不需要像在JavaScript中那样用Objective-C包装第二个立即执行的闭包。

答案 1 :(得分:2)

如果要返回块,则需要先发送copy消息或使用Block_copy功能将其复制。为防止内存泄漏,您必须稍后释放复制的块,例如使用autorelease

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