معالجة الأخطاء التي لا يمكن إصلاحها في سويفت

مقدمة


هذا المقال مثال على كيفية القيام بالبحث في وظائف Swift Standard Library بناء سلوكنا ليس فقط على وثائق المكتبة ولكن أيضًا على الكود المصدر.


أخطاء غير قابلة للاسترداد


يمكن فصل جميع الأحداث التي يسميها المبرمجون "الأخطاء" إلى نوعين.


  • الأحداث الناتجة عن عوامل خارجية مثل فشل اتصال الشبكة.
  • الأحداث الناجمة عن خطأ مبرمج مثل الوصول إلى حالة مشغل التبديل والتي يجب أن تكون غير قابلة للوصول.

تتم معالجة أحداث النوع الأول في تدفق تحكم منتظم. على سبيل المثال ، نحن نتفاعل مع فشل الشبكة من خلال عرض رسالة على المستخدم وتعيين تطبيق لانتظار استرداد اتصال الشبكة.


نحاول اكتشاف الأحداث من النوع الثاني والقضاء عليها في أقرب وقت ممكن قبل انتقال الرمز إلى الإنتاج. أحد الأساليب هنا هي تشغيل بعض عمليات التحقق من وقت التشغيل لإنهاء تنفيذ البرنامج في حالة يمكن تصحيحها وطباعة رسالة مع الإشارة إلى مكان حدوث الخطأ في التعليمات البرمجية.


على سبيل المثال ، يمكن للمبرمج إنهاء التنفيذ إذا لم يتم توفير المُهيئ المطلوب ولكن تم استدعاؤه. هذا سوف يكون لاحظت دائما وثابتة خلال التشغيل التجريبي الأول.


required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 

مثال آخر هو التبديل بين المؤشرات (دعنا نفترض أنه لسبب ما لا يمكنك استخدام التعداد).


 switch index { case 0: // something is done here case 1: // other thing is done here case 2: // and other thing is done here default: assertionFailure("Impossible index") } 

مرة أخرى ، سوف يتسبب المبرمج في حدوث عطل أثناء تصحيح الأخطاء هنا من أجل ملاحظة حدوث خطأ في الفهرسة.


هناك خمس وظائف إنهاء من مكتبة Swift القياسية (كما في Swift 4.2).


 func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

 func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

أي من وظائف الإنهاء الخمسة يجب أن نفضلها؟


شفرة المصدر مقابل التوثيق


لنلقِ نظرة على الكود المصدري . يمكننا أن نرى ما يلي على الفور:


  1. كل من هذه الوظائف الخمس إما إنهاء تنفيذ البرنامج أو لا يفعل شيئا.
  2. إنهاء ممكن يحدث بطريقتين.
    • مع طباعة رسالة تصحيح مريحة عن طريق استدعاء _assertionFailure(_:_:file:line:flags:) .
    • بدون رسالة تصحيح فقط عن طريق استدعاء Builtin.condfail(error._value) أو Builtin.int_trap() .
  3. يكمن الاختلاف بين وظائف الإنهاء الخمسة في الظروف التي يحدث فيها كل ما سبق.
  4. fatalError(_:file:line) _assertionFailure(_:_:file:line:flags:) دون قيد أو شرط.
  5. تقوم وظائف الإنهاء الأربعة الأخرى بتقييم الشروط عن طريق استدعاء وظائف تقييم التكوين التالية. (تبدأ بتسطير أسفل السطر مما يعني أنها داخلية ولا يُفترض أن يتم استدعاؤها مباشرةً بواسطة مبرمج يستخدم مكتبة Swift القياسية).
    • _isReleaseAssertConfiguration()
    • _isDebugAssertConfiguration()
    • _isFastAssertConfiguration()

الآن دعونا نلقي نظرة على الوثائق . يمكننا أن نرى ما يلي على الفور.


  1. fatalError(_:file:line) يطبع رسالة معينة دون قيد أو شرط ويوقف التنفيذ .
  2. تختلف تأثيرات وظائف الإنهاء الأربعة الأخرى اعتمادًا على علامة -Onone المستخدمة: -Onone ، -O ، -Ounchecked . على سبيل المثال ، انظر إلى preconditionFailure(_:file:line:) documentation .
  3. يمكننا تعيين علامات SWIFT_OPTIMIZATION_LEVEL هذه في Xcode من خلال إعداد إنشاء برنامج التحويل البرمجي SWIFT_OPTIMIZATION_LEVEL .
  4. نعلم أيضًا من وثائق Xcode 10 أنه يتم تقديم علامة تحسين واحدة أخرى - -Osize -.
  5. وبالتالي لدينا أربعة أعلام بناء الأمثل للنظر.
    • -Onone (لا تحسن)
    • -O (الأمثل للسرعة)
    • -Osize (الأمثل للحجم)
    • -Ounchecked (قم بإيقاف تشغيل العديد من عمليات التحقق من برنامج التحويل البرمجي)

قد نستنتج أن التكوين الذي تم تقييمه في وظائف الإنهاء الأربعة يتم تعيينه بواسطة إشارات البناء هذه.


تشغيل وظائف تقييم التكوين


على الرغم من أن وظائف تقييم التهيئة مصممة للاستخدام الداخلي ، إلا أن بعضها عام لأغراض الاختبار ، وقد نجربها من خلال CLI مع إعطاء الأوامر التالية في Bash.


 $ echo 'print(_isFastAssertConfiguration())' >conf.swift $ swift conf.swift false $ swift -Onone conf.swift false $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift true 

 $ echo 'print(_isDebugAssertConfiguration())' >conf.swift $ swift conf.swift true $ swift -Onone conf.swift true $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift false 

تقودنا هذه الاختبارات وفحص الكود المصدري إلى الاستنتاجات الأولية التالية.


هناك ثلاثة تكوينات متبادلة الحصري.


  • يتم تعيين تكوين الإصدار من خلال توفير علامة بناء -O أو علامة حجم.
  • يتم تعيين تكوين تصحيح الأخطاء عن طريق توفير علامة بناء -Onone أو لا توجد علامات تحسين على الإطلاق.
  • _isFastAssertConfiguration() يتم تقييمه إلى true إذا تم تعيين علامة -Ounchecked يتم -Ounchecked . على الرغم من أن هذه الوظيفة تحتوي على كلمة "سريع" في اسمها ، إلا أنها لا علاقة لها بالتحسين من أجل علامة بناء السرعة.

ملاحظة: هذه الاستنتاجات ليست هي التعريف الصارم للوقت الذي يتم فيه إنشاء تصحيح الأخطاء أو إنشاء الإصدار . إنها قضية أكثر تعقيدًا. لكن هذه الاستنتاجات صحيحة لسياق إنهاء استخدام الدالات.


تبسيط الصورة


-Ounchecked


لنلقِ نظرة على ما هي العلامة التي لم يتم -Ounchecked منها (غير ذات صلة هنا) ، ولكن ما هو دورها في سياق إنهاء استخدام الدالات.


  • وثائق precondition(_:_:file:line:) assert(_:_:file:line:) تقول ، "في -Ounchecked لم يتم -Ounchecked ، لا يتم تقييم الشرط ، ولكن قد يفترض المُحسِّن أنه يتم تقييمه دائمًا إلى true. الفشل في تلبية هذا الافتراض هو خطأ خطير في البرمجة. "
  • وثائق ل preconditionFailure(_:file:line) و assertionFailure(_:file:line:) تقول ، "في -Ounchecked البناء التي لم يتم -Ounchecked ، قد يفترض المُحسِّن أن هذه الوظيفة لا يُطلق عليها أبدًا. الفشل في تلبية هذا الافتراض خطأ خطير في البرمجة. "
  • يمكننا أن نرى من التعليمات البرمجية المصدر أن تقييم _isFastAssertConfiguration() إلى true يجب ألا يحدث . (إذا حدث ذلك ، _conditionallyUnreachable() . انظر راجع السطور 136 و _conditionallyUnreachable() )

التحدث مباشرة ، يجب ألا تسمح بالوصول إلى وظائف الإنهاء الأربعة التالية مع علامة البناء التي لم يتم -Ounchecked لتعيين البرنامج.


  • precondition(_:_:file:line:)
  • preconditionFailure(_:file:line)
  • assert(_:_:file:line:)
  • assertionFailure(_:file:line:)

استخدم فقط fatalError(_:file:line) أثناء تطبيق -Ounchecked وفي الوقت نفسه السماح بأن نقطة برنامجك مع fatalError(_:file:line) قد تكون قابلة للوصول.


دور التحقق من حالة


اثنين من وظائف إنهاء تتيح لنا التحقق من الظروف. يسمح لنا فحص الكود المصدري برؤية أنه في حالة فشل الشرط ، يكون سلوك الوظيفة هو نفس سلوك ابن عمها:


  • precondition(_:_:file:line:) يصبح preconditionFailure(_:file:line) ،
  • assert(_:_:file:line:) يصبح assertionFailure(_:file:line:) .

تلك المعرفة تجعل التحليل أكثر سهولة.


الافراج عن مقابل تكوينات التصحيح


في النهاية ، يسمح لنا المزيد من الوثائق وفحص الكود المصدري بصياغة الجدول التالي.


وظائف إنهاء


أصبح من الواضح الآن أن الخيار الأكثر أهمية للمبرمج هو سلوك البرنامج الذي يجب أن يكون عليه في الإصدار إذا كشف فحص وقت التشغيل عن خطأ.


إن الوجبات الرئيسية هنا هي أن assert(_:_:file:line:) و assertionFailure(_:file:line:) يجعل تأثير فشل البرنامج أقل حدة. على سبيل المثال ، ربما أفسد تطبيق iOS واجهة المستخدم (نظرًا لفشل بعض عمليات التحقق المهمة في وقت التشغيل) ولكنه لن يتعطل.


لكن هذا السيناريو قد لا يكون هو السيناريو الذي تريده. لديك خيار.


Never العودة النوع


Never يتم استخدامه Never كنوع مسترجع من الوظيفة التي تقوم بإلقاء خطأ دون قيد أو شرط أو مصائد أو لا تنتهي بطريقة طبيعية. هذه الأنواع من الوظائف لا ترجع فعليًا ، فهي لن تعود أبدًا .


من بين وظائف الإنهاء الخمسة ، فقط preconditionFailure(_:file:line) و fatalError(_:file:line) Never لأن هاتين الدالتين فقط fatalError(_:file:line) عن تنفيذ البرنامج دون قيد أو شرط ، وبالتالي لا تُرجع أبدًا.


فيما يلي مثال رائع لاستخدام " Never الكتابة" في تطبيق سطر الأوامر. (على الرغم من أن هذا المثال لا يستخدم دالات إنهاء مكتبة Swift القياسية ، إلا أنه يستخدم الدالة C exit() القياسية بدلاً من ذلك).


 func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() } // ... 

إذا قامت printUsagePromptAndExit() بإرجاع Void بدلاً من Never ، فسوف تحصل على خطأ في وقت البناء مع الرسالة ، " يجب ألا يتدخل نص" guard "، ضع في اعتبارك استخدام" return "أو" رمي "للخروج من النطاق ". باستخدام Never فأنت تقول مقدمًا أنك لن تخرج من النطاق أبدًا وبالتالي فإن المترجم لن يمنحك خطأ وقت البناء. خلاف ذلك ، يجب عليك إضافة return في نهاية كتلة رمز الحماية ، والتي لا تبدو لطيفة.


الوجبات الجاهزة


  • لا يهم أي وظيفة إنهاء لاستخدامها إذا كنت متأكدًا من أن جميع عمليات التحقق من وقت التشغيل مناسبة فقط لتكوين تصحيح الأخطاء .
  • استخدم فقط fatalError(_:file:line) أثناء -Ounchecked في الوقت نفسه ، وتم -Ounchecked من إمكانية الوصول إلى نقطة البرنامج مع fatalError(_:file:line) .
  • استخدم assert(_:_:file:line:) و assertionFailure(_:file:line:) إذا كنت قلقًا من أن اختبارات وقت التشغيل قد تفشل بطريقة ما في الإصدار. على الأقل لن يتعطل تطبيقك.
  • استخدم Never لتجعل الكود يبدو أنيقًا.


Source: https://habr.com/ru/post/ar440884/


All Articles