我开始大量使用块很快就注意到nil块会导致总线错误:
typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error
这似乎违反了Objective-C的通常行为,忽略了对nil对象的消息:
NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine
因此,在使用区块之前,我必须采用通常的零检查:
if (aBlock != nil)
aBlock();
或使用虚拟块:
aBlock = ^{};
aBlock(); // runs fine
还有其他选择吗?有没有理由为什么nil block不能简单地成为nop?
答案 0 :(得分:139)
我想用更完整的答案解释一下这个问题。首先让我们考虑一下这段代码:
#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消息已经通过蹦床,因为他们需要查找实际调用的方法。运行时允许延迟注入方法和更改方法实现,因此无论如何它已经通过蹦床了。在这种情况下,进行零检查的额外惩罚并不重要。
我希望这有助于解释一下理由。
答案 1 :(得分:39)
Matt Galloway的回答非常完美!好读!
我只想补充说,有一些方法可以让生活更轻松。您可以像这样定义一个宏:
#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;
这样您就可以轻松定义回退值。在我的例子中'nil'。
答案 2 :(得分:8)
阻止 objective-c objects但是调用了block is not a message,尽管您仍然可以[block retain]
nil
阻止或其他消息。
希望那(和链接)有所帮助。
答案 3 :(得分:2)
这是我最简单的最好的解决方案......也许有可能用那些c var-args编写一个通用运行函数,但我不知道怎么写。
void run(void (^block)()) {
if (block)block();
}
void runWith(void (^block)(id), id value) {
if (block)block(value);
}