برنامه نویسی 287 بازدید

در این بخش از سری مقالات آموزش برنامه‌نویسی سوئیفت به موضوع مدیریت خطا خواهیم پرداخت. در بخش قبلی در مورد ژنریک ها در این زبان برنامه‌نویسی صحبت کردیم که می‌توانید با کلیک روی لینک زیر آن را مطالعه کنید:

مقدمه

آیا تاکنون با این موقعیت مواجه شده‌اید که برای آماده‌سازی غذا به آشپزخانه بروید و متوجه شوید که هیچ ظرف تمیزی ندارید؟ در این حالت چند گزینه پیش روی شما است:

  1. از شام خوردن منصرف شوید.
  2. به یک رستوران بروید.
  3. ظرف‌ها را تمیز کنید و سپس به آماده‌سازی شام در خانه بپردازید.

این تصمیم‌ها نه‌تنها روی شما بلکه روی همه افرادی که برای آن‌ها غذا تهیه می‌کنید تأثیر خواهند گذاشت. گزینه A ساده‌ترین راه‌حل است؛ اما ممکن است اعضای دیگر خانواده گرسنه باشند و منتظر باشند که شما برای آن‌ها شام تهیه کنید. گزینه B نیز آسان است؛ اما هزینه بالایی دارد و به کار بیشتری نیاز دارد که الزامی برای آن وجود ندارد و ممکن است مزه غذای بیرون به اندازه غذایی که در خانه پخته می‌شود، خوب نباشد. گزینه C مسیر دشوار است؛ اما در صورتی که بخواهید مزه غذای شما خوب باشد و شکایت کمتری بشنوید، بهترین گزینه است.

اما شاید بپرسید همه این مطالب چه ارتباطی به موضوع مدیریت خطا دارند؟ در واقع زمانی که تلاش می‌کنید ظرف‌ها را بشورید و شام را آماده کنید، یک متد را فراخوانی می‌کنید که الگوریتم آن به شرح زیر است:

func fixDinner() {
    let dishes = getDishes()
    let pans = getPans()
    let food = getFood()
    let preparedFood = prepare(food, with: pans)
    
    serve(preparedFood)
}

ما در اغلب موارد در مورد مدیریت خطا تفکر چندانی نمی‌کنیم. اگر اپلیکیشن ما واقعاً مانند کد فوق باشد، تنها نیمی از افراد غذا دریافت می‌کنند و بقیه افراد با بشقاب خالی مواجه می‌شوند.

ما این کارها را در زندگی روزمره چنان به صورت طبیعی انجام می‌دهیم که وقتی وارد دنیای برنامه‌نویسی می‌شویم، برخی از برنامه نویسان اجازه می‌دهند برنامه‌هایشان از کار بیفتد و فکر می‌کنند این وضعیت تقصیر کاربر بوده است. اما این تصور نادرستی است. از کار افتادن برنامه وقتی فرد دیگری از آن استفاده می‌کند، همواره تقصیر شما است. دلیل این که تقصیر شما است، این است که شما خطا را به درستی اداره نکرده‌اید. تیم‌های QA به همین دلیل در کسب‌وکارها تشکیل می‌یابند، تا مطمئن شوند که کاربران با خطاهای مدیریت‌نشده‌ای مواجه نخواهند شد.

نکات مهم مدیریت خطا

پنج نکته عمده وجود دارند که در کد فوق ممکن است با خطا مواجه شوند و هر کدام از آن‌ها ممکن است چند نکته خطا در خود داشته باشد. بدون وجود اطلاعات جانبی، یافتن منشأ خطا می‌تواند مبهم باشد. اما پنج نکته اصلی را می‌توان به صورت زیر فهرست‌بندی کرد:

  1. هیچ بشقابی وجود نداشته باشد.
  2. هیچ ماهیتابه‌ای وجود نداشته باشد.
  3. هیچ غذایی وجود نداشته باشد.
  4. شام سوخته باشد.
  5. هنگام تهیه غذا یکی از بشقاب‌ها را بشکنید.

این همان نقطه‌ای است که مدیریت خطا به کار می‌آید. روشی که ما برای مدیریت خطا استفاده می‌کنیم، این است که همه چیز را به صورت یک optional انتساب می‌دهیم و سپس با استفاده از گزاره‌های زیر آن را مدیریت می‌کنیم.

  • if let
  • (if!(something.count > 0
  • یا guard

اما اگر خواسته باشیم در همین لحظه یک پاسخ قطعی داشته باشیم، گزاره‌ای به نام do/catch نیز وجود دارد.

do {
    try serveDinner()     // may fail so we use try in front of it
} catch let error {       // catch the error so we can use it below
    print(error.localizedDescription)   // print the error
}

قالب گزاره do/catch بدین ترتیب است که ابتدا در بخش do کاری انجام می‌یابد و در صورتی که در موردی احتمال بروز خطا باشد از بخش try برای دریافت آن مقدار و بازگشت دادن آن یا انجام کار خاصی استفاده می‌کنیم. اگر متد موفق باشد، از گزاره do/catch خارج می‌شویم. اگر ناموفق باشد در این صورت بخش catch خطا را دریافت کرده و یک نام به آن می‌دهد تا بتوان آن را در بدنه catch با استفاده از let error استفاده کرد. این که آن را چه بنامیم اهمیت چندانی ندارد، شما می‌توانید error را به هر چیزی که می‌خواهید تغییر دهید، کافی است بدانید که نوع آن یک خطا (Error) است. مشخصه localizedDescription در این Error صرفاً یک مشخصه محاسبه شده است که یک توصیف متنی از آن چه که موجب خطا شده است ارائه می‌کند.

شما می‌توانید Let error را نادیده بگیرید و در این حالت سوئیفت به صورت خودکار یک ثابت error به شما ارائه می‌کند که می‌توانید از آن در بدنه catch استفاده کنید.

گزینه try

اگر بخواهیم در مورد try نیز صحبت کنیم، باید بگوییم که سه نوع متفاوت از try وجود دارد که می‌توان مورد استفاده قرار داد و در ادامه توضیح داده‌ایم.

  • try: تلاش می‌کند تا تابع مربوطه را اجرا کند و یا نتیجه را بازگشت دهد و یا در صورت ناموفق بودن خطا را دریافت کند.
  • ?try: تلاش می‌کند تابع مربوطه را اجرا کند و اگر موفق باشد، مقدار غیر optimal را بازگشت می‌دهد. در غیر این صورت در مورد خطاهای ایجاد شده مسئولیتی ندارد.
  • !try: تلاش می‌کند تابع مربوطه را اجرا کند و تصورمی کند که خطایی ایجاد نخواهد شد و در صورتی هم که خطا ایجاد شود، مشکلی با از کار افتادن اپلیکیشن ندارد و به مدیریت خطا نمی‌پردازد.

بنابراین دو گزینه اول عالی هستند؛ گزینه سوم صرفاً زمانی مفید است که کاملاً مطمئن باشید متد مربوطه هرگز با خطا مواجه نخواهد شد. مثال خوبی از این وضعیت، متد (String(contentsOf: است که یک فایل را از بسته اپلیکیشن شما می‌خواند و در صورتی که نتواند فایل را پیدا کند، این مشکل کاربر خواهد بود که محتوای بسته را دستکاری کرده است.

از این مورد می‌توان برای بررسی هَش‌های فایل استفاده کرد و بدین ترتیب اطمینان یافت که کاربر چیزی را تغییر نداده است. اگر کاربر چنین کاری را انجام داده باشد، برنامه به حالت غیر قابل استفاده در می‌آید و آن‌ها مجبور می‌شوند تا یک کپی جدیدی از برنامه را از منبع مربوطه دریافت کنند.

هنگامی که از ?try یا !try استفاده می‌کنید لازم نیست کد را درون یک گزاره do/catch قرار دهید.

صدور خطا چگونه رخ می‌دهد؟

خطاها با الحاق عبارت throws پس از پارامترهای اعلان تابع صادر می‌شوند.

func myNonWorkingFunction(someParam: Int) throws {
    do {
        try stuff(using: someParam)
    } catch {
        throw error
    }
}

اگر بخواهید خطاهای خاص خود را ایجاد کنید، امکان این کار وجود دارد. کافی است یک enum ایجاد کنید که از پروتکل Error استفاده می‌کند.

enum MyAppSpecificErrors {
    case nonWorkingFunctionExecuted
    case undefined
    case fileDoesNotExist
    case createFileError
    case openFileError
}

case openFileError

}

وقتی که از throw استفاده می‌کنیم، می‌توانیم از MyAppSpecificErrors.openFileError برای نشان دادن این نکته که فایل نمی‌تواند باز شود استفاده کنیم. با این وجود، یک «توصیف محلی» (localizedDescription) برای آن وجود ندارد. بنابراین نمی‌توانیم این عبارت را به کاربر بازگشت دهیم مگر این که وی کاملاً یک فرد فنی باشد. اگر بخواهید بدین ترتیب از آن استفاده کنید باید یک do/catch مانند زیر طراحی کنید:

do {
   try somethingBreaking()
} catch MyAppSpecificErrors.openFileError {
   print("Could not open file")
} catch MyAppSpecificErrors.nonWorkingFunctionExecuted {
   print("Oops!")
} catch MyAppSpecificErrors.createFileError {
   print("Could not create a new file")
} catch {
   print("Something went wrong")
}

در مثال فوق، گزاره‌های catch متفاوتی برای هر شکست بالقوه داریم.

مثال: باز کردن فایل

اگر در باز کردن یک فایل ناموفق باشیم، باید بررسی کنیم که آیا آن فایل وجود دارد یا نه و این بررسی ربطی به بررسی قبلی که در زمان اقدام به باز کردن فایل انجام داده‌ایم ندارد. اگر فایل موجود نباشد، می‌توانیم آن را ایجاد کرده و دوباره امتحان کنیم. اگر فایل موجود باشد، در این صورت باید یک گزارش به کاربر بازگشت دهیم و در آن اعلام کنیم که باید فایل را ببندد تا ما بتوانیم از آن استفاده کنیم.

در واقع شما باید گزاره‌های catch را برای خطاهایی بنویسید که می‌توانند صادر شوند؛ اما نباید از یک گزاره catch نهایی برای همه موارد غیر معین استفاده کنید.

اگر بخواهیم رشته خطا را از تابع صادرکننده آن بازگشت دهیم، یک گزینه در اختیار ما قرار گرفته است که قبلاً به آن اشاره کردیم. این گزینه localizedDescription نام دارد و برای همه خطاهای اپل وجود دارد؛ اما روش کار آن به این صورت نیست که متن fileOpenError را بخواند و به صورتی جادویی آن را به صورت «فایل نمی‌تواند باز شود» (Unable to open file) ترجمه کند. ما باید به آن اعلام کنیم که چه چیزی را بیان کند.

enum FileErrors: String, Error {
   case kWriteFailure = "Unable to write to file"
   case kOpenFailure = "Unable to open file"
  
   var localizedDescription: String {
      return self.rawValue
   }
}

در کد فوق ما یک enum داریم که برای پوشش دادن FileErrors-هایی استفاده می‌شود که از String و Error بهره می‌گیرند. ما می‌خواهیم از String برای ارائه مقادیر متنی برای خطاها استفاده کنیم.

در این صورت ما دو حالت داریم که هر کدام متن انتسابی خاص خود را به صورت rawValue دارند. بدین ترتیب می‌توانیم یک مشخصه محاسبه‌شده با یک نام مناسب داشته باشیم. در این مثال ما نام آن را localizedDescription تعیین می‌کنیم و نوع آن را نیز String قرار می‌دهیم.

زمانی که فردی FileErrors.kOpenFailure.localizedDescription را فراخوانی کند، با مقدار خام kOpenFailure مواجه می‌شود که مقدار رشته‌ای آن به صورت “Unable to open file” است.

در واقع ما در ابتدا localizedDescription را throw نمی‌کنیم؛ بلکه صرفاً خطا را صادر می‌کنیم و زمانی که در ادامه localizedDescription را روی خطا فراخوانی کنیم، همچنان خطای خام را دریافت می‌کنیم. مقدار خام می‌تواند از نوع String ،Character ،Integer یا FloatingType باشد.

Enum-ها یک ویژگی جالب دارند که وقتی با یک عدد صحیح برای مقادیر خام آغاز می‌کنید و می‌خواهید آن را مقیاس‌بندی کنید، کافی است صرفاً مقدار اولیه را ایجاد کنید و موارد دیگر به صورت خودکار با اعداد صحیح بعدی تأمین می‌شوند. در بخش بعدی این سری مقالات در مورد Enum-ها بیشتر صحبت می‌کنیم؛ اما فعلاً صرفاً به مرور مواردی که آموختیم می‌پردازیم.

rethrows

یک نکته دیگر نیز وجود دارد که باید در مورد صادر کردن تابع‌ها بدانید و آن کلیدواژه rethrows است.

به خاطر دارید که وقتی در بخش‌های قبلی در مورد کلوژرها صحبت می‌کردیم آن‌ها را به صورت تابعی تعریف کردیم که تابع دیگری را به عنوان پارامتر می‌گیرند. اگر تابع ارسالی به صورت یک پارامتر، تابعی از نوع throwing باشد، از کلیدواژه rethrows به جای throws استفاده می‌کنیم تا نشان دهیم که تابع خطایی را صادر می‌کند که توسط یک تابع پارامتری صادر شده است. مثالی از آن را در ادامه ملاحظه می‌کنید:

func throwingFunction(_ number: Int) -> throws { ... }

func hopeThisWorks(_ aFunc: (Int) throws -> () ) rethrows -> Bool {
    do {
        try aFunc(5)
    } catch {
        throw error
    }
}

do {
    try hopeThisWorks(throwingFunction(5))
} catch {
    print(error.localizedDescription)
}

throwingFunction یک Int می‌گیرد و با throw نشانه‌گذاری می‌شود. این وضعیت با امضای پارامتر که hopeThisWorks برای پارامترهایش استفاده می‌کند مطابقت دارد.

hopeThisWorks در یک گزاره do/catch با استفاده از try فراخوانی می‌شود. در ادامه (throwingFunction(5 به عنوان پارامتر ارسال می‌شود. اما اگر فرض کنیم که throwingFunction مقدار 3 را می‌گیرد، در این حالت خطایی صادر می‌شود که می‌تواند در hopeThisWorks دریافت شود و سپس hopeThisWorks می‌تواند خطا را مجدداً به جایی که از آن صادر شده بود rethrow کند.

در آغاز این مقاله به شما گفتیم که سه گزینه مختلف دارید. وقتی آن گزینه‌ها را به زبان اپلیکیشن ترجمه کنیم، به حالت زیر درمی‌آیند:

  • اجازه بده اپلیکیشن از کار بیافتد.
  • از یک کتابخانه شخص ثالث که امکانات مدیریت خطای بیشتری دارد برای دیباگ استفاده کن.
  • کد لازم برای مدیریت خطا را شخصاً بنویس.

تا قبل از مطالعه این مقاله ممکن بود گزینه‌های الف و ب را امتحان کنید. با مطالعه این مقاله با روش سوم برای نوشتن کدهای مدیریت خطا نیز آشنا شدید.

جمع‌بندی

طراحی مقدماتی یک اپلیکیشن بدون استفاده از رویه‌های مدیریت خطا اشکالی ندارد. در این حالت همه جاهایی که به مدیریت خطا نیاز دارند را با استفاده از کامنت TODO: Handle Error// نشانه‌گذاری کنید. بدین ترتیب می‌توانید زمانی که کار کدنویسی پایان یافت به کد خود بازگردید و خطاهایی که ممکن است در موارد غیر ایده‌آل پیش بیایند را مدیریت کنید. زمانی که اپلیکیشن خود را تست می‌کنید در صورتی که با خطایی مواجه شدید، آن را مدیریت کنید.

بدین ترتیب در این مقاله با روش دریافت خطا، روش صادر کردن خطا، روش ایجاد خطا و تعیین توصیف‌هایی برای حالت‌های مختلف خطا آشنا شدیم. همچنین شیوه صدور مجدد خطاها را آموختیم.

در بخش بعدی این سری مقالات آموزش سوئیفت با روش استفاده از Enum–ها به همراه ژنریک‌ها و بستارها (Closure) آشنا خواهیم شد و با برخی از کاربردهای پیشرفته‌تر Enum-ها مانند استفاده از بستار در حالت‌های مختلف صحبت خواهیم کرد. در این حالت از ژنریک‌ها نیز استفاده می‌شود.

برای مطالعه قسمت بعدی این مجموعه مطلب آموزشی روی لینک زیر کلیک کنید:

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *