5

这是一个棘手的问题,我无法解决。

我知道 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 是通过引用而不是值传递的。

如果我们想“修复”这个问题并让每个函数在创建时使用 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]();

它不是直接创建最终函数,而是使用中间闭包返回最终函数,其中正确的值“固定”在其中;因此将返回:

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

但是它在编译时给出了以下错误:Returning block that lives on the local stack

谢谢,最好的;D!

4

2 回答 2

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 中包装立即执行的第二个闭包来执行此操作。

于 2012-08-21T08:40:14.747 回答
2

如果你想返回一个块,你需要先复制它,通过发送copy消息或使用Block_copy函数。为了避免泄漏内存,您必须稍后释放复制的块,例如使用autorelease

for(;i<8;++i) {
    [_arr addObject:^(int new_i){
        return [[^{
            NSLog(@"Result: %i", new_i);
        } copy] autorelease];
    }(i)];
}
于 2012-08-21T05:50:14.683 回答