2010-11-10 24 views
67

tôi bắt đầu sử dụng các khối rất nhiều và sớm nhận thấy rằng khối nil gây ra lỗi xe buýt:Tại sao khối nil/NULL gây ra lỗi bus khi chạy?

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

Điều này dường như đi ngược lại với hành vi thông thường của Objective-C mà bỏ qua thông điệp đến đối tượng nil:

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

Vì vậy, tôi phải nghỉ mát để thường lệ bằng không kiểm tra trước khi tôi sử dụng một khối:

if (aBlock != nil) 
    aBlock(); 

Hoặc sử dụng các khối giả:

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

Có tùy chọn nào khác không? Có một lý do tại sao các khối nil không thể đơn giản là nop?

Trả lời

134

Tôi muốn giải thích thêm một chút nữa, với câu trả lời hoàn chỉnh hơn. Đầu tiên chúng ta hãy xem xét mã này:

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

Nếu bạn chạy này sau đó bạn sẽ thấy một vụ tai nạn trên dòng block() trông giống như thế này (khi chạy trên một kiến ​​trúc 32-bit - đó là quan trọng):

EXC_BAD_ACCESS (mã = 2, địa chỉ = 0xc)

Vì vậy, tại sao lại như vậy? Vâng, 0xc là bit quan trọng nhất.Sự cố có nghĩa là bộ xử lý đã cố gắng đọc thông tin tại địa chỉ bộ nhớ 0xc. Điều này gần như chắc chắn là một điều hoàn toàn không chính xác để làm. Không có gì ở đó cả. Nhưng tại sao nó cố gắng đọc vị trí bộ nhớ này? Vâng, đó là do cách mà một khối thực sự được xây dựng dưới mui xe.

Khi một khối được xác định, trình biên dịch thực sự tạo ra một cấu trúc trên stack, của mẫu này:

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

Khối là sau đó một con trỏ đến cấu trúc này. Thành viên thứ tư, invoke, cấu trúc này là một điều thú vị. Nó là một con trỏ hàm, trỏ đến mã nơi thực thi của khối được giữ. Vì vậy, bộ xử lý cố gắng nhảy đến mã đó khi một khối được gọi. Lưu ý rằng nếu bạn đếm số byte trong cấu trúc trước thành viên invoke, bạn sẽ thấy rằng có 12 chữ số thập phân hoặc C trong hệ thập lục phân.

Vì vậy, khi một khối được gọi, bộ xử lý lấy địa chỉ của khối, thêm 12 và cố gắng tải giá trị được giữ tại địa chỉ bộ nhớ đó. Sau đó nó cố gắng chuyển đến địa chỉ đó. Nhưng nếu khối là nil thì nó sẽ cố đọc địa chỉ 0xc. Đây là một địa chỉ duff, rõ ràng, và vì vậy chúng tôi nhận được lỗi phân đoạn.

Bây giờ lý do nó phải là một vụ tai nạn như thế này chứ không phải âm thầm không giống như một cuộc gọi tin nhắn mục tiêu-C thực sự là một sự lựa chọn thiết kế. Vì trình biên dịch đang thực hiện công việc quyết định cách gọi khối, nó sẽ phải tiêm mã kiểm tra nil ở mọi nơi mà một khối được gọi. Điều này sẽ làm tăng kích thước mã và dẫn đến hiệu suất kém. Một lựa chọn khác là sử dụng tấm bạt lò xo để kiểm tra không. Tuy nhiên điều này cũng sẽ phải chịu hình phạt về hiệu suất. Các thư mục tiêu-C đã trải qua một tấm bạt lò xo vì chúng cần tra cứu phương thức thực sự sẽ được gọi. Thời gian chạy cho phép tiêm các phương thức lười biếng và thay đổi phương thức triển khai phương thức, vì vậy nó đã trải qua một tấm bạt lò xo. Hình phạt bổ sung của việc kiểm tra nil là không đáng kể trong trường hợp này.

Tôi hy vọng rằng sẽ giúp một chút để giải thích lý do.

Để biết thêm thông tin, hãy xem blogposts của tôi.

8

Lưu ý: Tôi không có chuyên gia về Chặn.

Blocks objective-c objects nhưng gọi một block is not a message, mặc dù bạn vẫn có thể thử [block retain] ing một khối nil hoặc tin nhắn khác.

Hy vọng rằng (và các liên kết) sẽ giúp ích.

+0

Cảm ơn bạn, các liên kết thú vị. Tôi biết rằng việc gọi một khối không giống như gửi cho nó một tin nhắn, nhưng về mặt khái niệm nó sẽ là tốt đẹp nếu các khối nil được tha thứ như các đối tượng nil. – zoul

+0

Bạn có thể thêm danh mục vào loại '__block' ... nhưng tôi không chắc chắn. '#define nilBlock^{}' cũng có thể làm cho cuộc sống của bạn dễ dàng hơn. –

+0

Tôi đã nghĩ về phương pháp 'nilBlock', rất tiếc việc nhập liệu được tạo ra - việc tạo một giá trị nil riêng biệt cho từng loại khối không vui lắm. – zoul

2

Đây là giải pháp đơn giản nhất của tôi… Có lẽ có thể viết một hàm chạy phổ quát với các hàm c var-arg nhưng tôi không biết cách viết nó.

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

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

Câu trả lời của Matt Galloway là hoàn hảo! Tuyệt vời đọc!

Tôi chỉ muốn thêm rằng có một số cách để làm cho cuộc sống dễ dàng hơn. Bạn có thể xác định macro như sau:

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

Có thể lấy 0 - n đối số. Ví dụ về việc sử dụng

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

Nếu bạn muốn nhận được giá trị trở lại của khối và bạn không chắc chắn nếu khối tồn tại hay không thì bạn có lẽ tốt hơn chỉ cần gõ:

block ? block() : nil; 

Bằng cách này bạn có thể dễ dàng xác định giá trị dự phòng. Trong ví dụ của tôi 'nil'.

+1

__VA_ARGS__ gây ra sự cố trong các tệp .mm – BergP

+0

@PavelKatunin Tôi chưa bao giờ thử nghiệm – hfossli

Các vấn đề liên quan