web-dev-qa-db-ja.com

実行時にnil / NULLブロックがバスエラーを引き起こすのはなぜですか?

私はたくさんのブロックを使い始めて、すぐにnilブロックがバスエラーを引き起こすことに気づきました:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

これは、nilオブジェクトへのメッセージを無視するObjective-Cの通常の動作に反するようです。

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

したがって、ブロックを使用する前に、通常のnilチェックに頼らなければなりません。

if (aBlock != nil)
    aBlock();

または、ダミーブロックを使用します。

aBlock = ^{};
aBlock(); // runs fine

別のオプションはありますか? nilブロックが単にnopではない理由はありますか?

73
zoul

これについてもう少し詳しく説明し、より完全な回答を示します。最初にこのコードを考えてみましょう:

_#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}
_

これを実行すると、次のようなblock()行でクラッシュが発生します(32ビットアーキテクチャで実行した場合-重要です)。

EXC_BAD_ACCESS(コード= 2、アドレス= 0xc)

それで、なぜですか?さて、_0xc_は最も重要なビットです。クラッシュは、プロセッサがメモリアドレス_0xc_の情報を読み取ろうとしたことを意味します。これはほぼ間違いなく、完全に正しくないことです。そこに何かがある可能性は低いです。しかし、なぜこのメモリ位置を読み取ろうとしたのでしょうか?まあ、それはブロックが実際にフードの下で構築される方法によるものです。

ブロックが定義されると、コンパイラーは実際にスタックに次の形式の構造を作成します。

_struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
_

ブロックは、この構造へのポインタです。この構造の4番目のメンバーinvokeは興味深いものです。これは、ブロックの実装が保持されているコードを指す関数ポインターです。したがって、プロセッサは、ブロックが呼び出されたときにそのコードにジャンプしようとします。 invokeメンバーの前の構造体のバイト数を数えると、10進数で12、16進数でCであることがわかります。

したがって、ブロックが呼び出されると、プロセッサはブロックのアドレスを取得し、12を加算して、そのメモリアドレスに保持されている値をロードしようとします。その後、そのアドレスにジャンプしようとします。しかし、ブロックがnilの場合、アドレス_0xc_を読み取ろうとします。これは明らかにダフアドレスであり、セグメンテーションエラーが発生します。

Objective-Cメッセージコールのように静かに失敗するのではなく、このようにクラッシュする必要がある理由は、実際には設計上の選択です。コンパイラーはブロックを呼び出す方法を決定する作業を行っているため、ブロックが呼び出されるすべての場所にnil検査コードを挿入する必要があります。これにより、コードサイズが増加し、パフォーマンスが低下します。別のオプションは、nilチェックを行うトランポリンを使用することです。ただし、これによりパフォーマンスが低下します。 Objective-Cメッセージは、実際に呼び出されるメソッドを検索する必要があるため、すでにトランポリンを通過しています。ランタイムは、メソッドの遅延注入とメソッド実装の変更を可能にするため、とにかくすでにトランポリンを通過しています。この場合、nilチェックを実行することによる余分なペナルティは重要ではありません。

それが根拠の説明に少し役立つことを願っています。

詳細については、my blogposts を参照してください。

139
mattjgalloway

マットギャロウェイの答えは完璧です。よく読んでください!

人生を楽にするいくつかの方法があることを付け加えたいだけです。次のようなマクロを定義できます。

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

0〜n個の引数を取ることができます。使用例

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

Ifブロックの戻り値を取得したいが、ブロックが存在するかどうかわからない場合は、次のように入力するだけの方がよいでしょう。

block ? block() : nil;

このようにして、フォールバック値を簡単に定義できます。私の例では「nil」です。

39
hfossli

警告:私はブロックの専門家ではありません。

ブロックobjective-c objects を呼び出しますが、 blockはではありませんメッセージ 、ただし[block retain]ing nilブロックまたはその他のメッセージ。

うまくいけば、それ(およびリンク)が役立ちます。

8
Stephen Furlani

これが私の最も簡単なすばらしい解決法です…多分それらのc var-argsで1つの普遍的な実行関数を書くことが可能ですが、私はそれを書く方法を知りません。

void run(void (^block)()) {
    if (block)block();
}

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}
2
Renetik