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 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 でこれを行うために、すぐに実行される 2 番目のクロージャーをラップする必要はありません。

于 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 に答える