Почему блоки nil / NULL вызывают ошибки шины при запуске?


Я начал использовать блоки много и вскоре заметил, что нулевые блоки вызывают ошибки шины:

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

это, кажется, идет вразрез с обычным поведением Objective-C, который игнорирует сообщения для нулевых объектов:

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

поэтому я должен прибегнуть к обычной нулевой проверке, прежде чем использовать блок:

if (aBlock != nil)
    aBlock();

или использовать фиктивные блоки:

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

есть другой вариант? Есть ли причина, почему nil blocks не может быть просто nop?

4 69

4 ответа:

Я хотел бы объяснить это немного больше, с более полный ответ. Сначала рассмотрим этот код:

#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. */
};

блок является указателем на эту структуру. Четвертый член,invoke, это структура-то интересная. Это указатель на функцию, указывающий на код, в котором выполняется реализация блока. Таким образом, процессор пытается перейти к этому коду при вызове блока. Обратите внимание, что если вы посчитаете количество байтов в структуре перед invoke член, вы обнаружите, что есть 12 в десятичном или C в шестнадцатеричном.

поэтому при вызове блока процессор берет адрес блока, добавляет 12 и пытается загрузить значение, хранящееся в этой памяти адрес. Затем он пытается перейти к этому адресу. Но если блок равен нулю, то он попытается прочитать адрес 0xc. Это адрес Даффа, ясно, и поэтому мы получаем ошибку сегментации.

теперь причина, по которой это должен быть сбой, а не молчаливый сбой, как вызов сообщения Objective-C, действительно является выбором дизайна. Поскольку компилятор выполняет работу по решению того, как вызвать блок, ему придется вводить код проверки nil везде, где вызывается блок. Этот увеличит размер кода и приведет к плохой производительности. Другой вариант - использовать батут, который делает нулевую проверку. Однако это также повлечет за собой штраф за производительность. Сообщения Objective-C уже проходят через батут, так как им нужно искать метод, который фактически будет вызван. Время выполнения позволяет лениво вводить методы и изменять реализации методов, поэтому он уже проходит через батут в любом случае. Дополнительное наказание за выполнение нулевой проверки не является значимый в данном случае.

Я надеюсь, что помогает немного пояснить.

для получения дополнительной информации см. my блогпосты.

ответ Мэтта Гэллоуэя идеален! Отличное чтение!

Я просто хочу добавить, что есть несколько способов, чтобы сделать жизнь проще. Вы можете определить макрос следующим образом:

#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");

если вы хотите получить возвращаемое значение блока и вы не уверены, если блок существует или нет, то вы, вероятно, лучше просто наберите:

block ? block() : nil;

таким образом, вы можете легко определить резервное значение. В моем примере "ноль".

предостережение: я не эксперт в блоках.

блоки areobjective-C objects но вызов a блок не ответ, хотя вы все еще можете попробовать [block retain]выбор nil блокировать или другие сообщения.

надеюсь, что (и ссылки) помогает.

Это мое простое самое приятное решение... может быть, можно написать одну универсальную функцию запуска с этими c var-args, но я не знаю, как это написать.

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

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