لماذا تتسبب الكتل NIL / NULL في أخطاء الحافلة عند التشغيل؟

StackOverflow https://stackoverflow.com/questions/4145164

  •  30-09-2019
  •  | 
  •  

سؤال

بدأت في استخدام الكتل كثيرًا وسرعان ما لاحظت أن الكتل لا تتسبب في أخطاء الحافلة:

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؟

هل كانت مفيدة؟

المحلول

أود أن أشرح هذا أكثر قليلاً ، مع إجابة أكثر اكتمالا. أولاً ، لننظر في هذا الرمز:

#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. هذا هو عنوان داف ، بوضوح ، وبالتالي نحصل على خطأ التجزئة.

الآن السبب في أنه يجب أن يكون حادثًا كهذا بدلاً من الفشل الصامت مثل استدعاء الرسائل الهدف-C هو حقًا اختيار تصميم. نظرًا لأن المترجم يقوم بعمل تحديد كيفية استدعاء الكتلة ، فسيتعين عليه حقن الكود في كل مكان يتم فيه استدعاء كتلة. هذا من شأنه أن يزيد من حجم الرمز ويؤدي إلى أداء سيء. هناك خيار آخر هو استخدام الترامبولين الذي يقوم بفحص لا شيء. ومع ذلك ، فإن هذا من شأنه أن يتحمل عقوبة الأداء. تمر رسائل Objective-C بالفعل من خلال الترامبولين لأنها بحاجة إلى البحث عن الطريقة التي سيتم استدعاؤها بالفعل. يسمح وقت التشغيل بالحقن الكسول للطرق وتغيير تطبيقات الطريقة ، لذلك يمر بالفعل من خلال الترامبولين على أي حال. العقوبة الإضافية المتمثلة في القيام بفحص لا شيء ليست مهمة في هذه الحالة.

آمل أن يساعد ذلك قليلاً في شرح الأساس المنطقي.

لمزيد من المعلومات ، راجع بلدي مقالات المشاركات.

نصائح أخرى

إجابة مات جالواي مثالية! قراءة رائعة!

أريد فقط أن أضيف أن هناك بعض الطرق لجعل الحياة أسهل. يمكنك تحديد ماكرو مثل هذا:

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

يمكن أن يستغرق 0 - ن الحجج. مثال على الاستخدام

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;

بهذه الطريقة يمكنك بسهولة تحديد قيمة الاحتياطية. في مثالي "لا شيء".

التحذير: لست خبيرًا في الكتل.

كتل نكون كائنات الهدف-C لكن استدعاء أ كتلة ليس رسالة, ، على الرغم من أنه لا يزال بإمكانك المحاولة [block retain]جي nil حظر أو رسائل أخرى.

نأمل أن يساعد ذلك (والروابط).

هذا هو حل أجمل بسيط ... ربما يكون هناك وظيفة تشغيل عالمية واحدة مع تلك C var-args لكنني لا أعرف كيف أكتب ذلك.

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

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top