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

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

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

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

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

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

if (aBlock != nil)
    aBlock();

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

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

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

Ответ 1

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

#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. Это, безусловно, адрес duff, и мы получаем ошибку сегментации.

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

Надеюсь, это поможет немного объяснить обоснование.

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

Ответ 2

Матт Гэллоуэй отвечает, это прекрасно! Отлично читаю!

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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