Почему блоки nil / NULL вызывают ошибки шины при запуске? - PullRequest
72 голосов
/ 10 ноября 2010

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

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

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

Ответы [ 4 ]

139 голосов
/ 05 февраля 2013

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

#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, на самом деле является выбором дизайна.Поскольку компилятор решает, как вызывать блок, он должен вводить нулевой проверочный код везде, где вызывается блок.Это увеличит размер кода и приведет к снижению производительности.Другой вариант - использовать батут, который проверяет ноль.Однако это также повлечет за собой снижение производительности.Сообщения Objective-C уже проходят через батут, так как им нужно найти метод, который на самом деле будет вызван.Среда выполнения позволяет лениво внедрять методы и изменять реализации методов, так что в любом случае он уже проходит через батут.Дополнительное наказание за выполнение нулевой проверки в этом случае не имеет значения.

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

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

39 голосов
/ 23 октября 2012

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

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

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

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

8 голосов
/ 10 ноября 2010

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

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

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

2 голосов
/ 07 мая 2012

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

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

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