代码之家  ›  专栏  ›  技术社区  ›  Ale Morales

Objective-C块,如何保存上下文值?

  •  5
  • Ale Morales  · 技术社区  · 13 年前

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

    我知道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中显示的行为? (i保留其在创建功能时的价值)

    我试着这样写循环:

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

    但在编译时会出现以下错误: 返回驻留在本地堆栈上的块

    感谢并致以最美好的祝愿;D

    2 回复  |  直到 13 年前
        1
  •  11
  •   newacct    13 年前

    你说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中包装第二个立即执行的闭包。

        2
  •  2
  •   Sven    13 年前

    如果你想返回一个块,你需要先复制它,或者发送 copy 消息或使用 Block_copy 作用为了防止内存泄漏,您必须稍后释放复制的块,例如使用 autorelease

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