در دنیای ایدهآل، همهچیز بینقص کار میکند. اما در دنیای واقعی، ممکن است خطاهایی رخ دهد، و نحوهی برخورد ما با این موقعیتها مهم است تا مطمئن شویم نرمافزارمان مقاوم و قابلاعتماد است. استثناها مفهومی حیاتی در برنامهنویسی هستند که به ما امکان میدهند خطاها و موقعیتهای پیشبینینشده را بهشکلی روان مدیریت کنیم.
ایجاد یک خطا، اگر مدیریت نشود، برنامه را متوقف میکند و یک پیام خطا بیرون میدهد. در بیشتر موارد، نمیخواهید برنامهتان هنگام بروز خطا متوقف شود؛ در عوض میخواهید خطا را مدیریت کنید و اجرای برنامه را ادامه دهید.
در Crystal، استثناها با «کلیدواژه»ی raise ایجاد میشوند و میتوان به آنها یک String یا یک شیء Exception داد.
اگر مدیریت نشود، برنامه متوقف میشود و پیام خطا را چاپ میکند.
raise "This is an error"
در Crystal چندین استثنای داخلی وجود دارد، مانند ArgumentError، IndexError، KeyError، IOError، SystemCallError، TypeError، ZeroDivisionError و بسیاری دیگر.
برای اینها باید پیامی به استثنا بدهید.
raise ArgumentError.new("This is an argument error")
نمیخواهیم برنامهمان وقتی استثنایی ایجاد میشود از کار بیفتد.
بنابراین، وقتی میدانیم بخشی از کد مستعد خطاست، میتوانیم آن را در یک «بلوک» begin بپیچیم و استثنا را با یک بلوک rescue بگیریم.
بلوک begin آغاز کدی را مشخص میکند که ممکن است استثنا ایجاد کند، و بلوک rescue استثنا را مدیریت میکند.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
بلوک rescue را میتوان با یک متغیر هم مشخص کرد تا شیء استثنا را دریافت کنید.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
بلوک rescue را میتوان تنها با یک نوع استثنای مشخص هم تعیین کرد تا فقط همان استثنا گرفته شود.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
میتوان از چند بلوک rescue برای مدیریت انواع مختلف استثناها استفاده کرد.
در مثال زیر، بلوک rescue اول یک ArgumentError را میگیرد و بلوک rescue دوم هر استثنای دیگری را میگیرد.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
بلوک begin میتواند یک بلوک else هم داشته باشد که در صورت ایجاد نشدن استثنا اجرا میشود.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
در پایان، یک بلوک ensure هست که همیشه اجرا میشود، فارغ از اینکه استثنایی ایجاد شده باشد یا نه.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
برخی متدها دو نسخه دارند، یکی با ! و دیگری بدون آن.
این میتواند دو معنای متفاوت داشته باشد.
یکی اینکه متد شیء را تغییر میدهد، و دیگری اینکه متد میتواند استثنا ایجاد کند.
اما قرارداد دیگری هم هست که مربوط به پایان یافتن نام متد با ? است و در مفهوم «منطقی» به آن اشاره شده است.
برخی متدها بهطور پیشفرض استثنا ایجاد میکنند، اما نسخهای هم دارند که با ? تمام میشود و بهجای ایجاد استثنا nil برمیگرداند.
این زمانی ایدهآل است که میخواهید از ایجاد یک خطا جلوگیری کنید. این میتواند به کارایی کمک کند، چون نیازی نیست «استکتریس» ساخته شود و اگر درست تنظیم شود، میتواند کد را امنتر کند.
همچنین میتوانید استثناهای خودتان را با ارثبری از کلاس Exception بسازید.
با این کار، میتوانید بهصورت اختیاری متد initialize را بازنویسی کنید تا پیام استثنا را تعیین کنید.
این کار را میتوان با اختصاص دادن پیام به یک متغیر نمونه به اسم @message انجام داد.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
روزی که مدتها منتظرش بودید بالاخره از راه رسید و شما اکنون صاحب مفتخر یک مزرعهی زیبا در آلپ هستید.
هنوز هم دوست ندارید صبح زود برای غذا دادن به گاوهایتان از خواب بیدار شوید.
چون مهندس زبردستی هستید، یک غذارسان میسازید به اسم FEED-M-ALL.
آخرین موردی که برای تمام کردن پروژهتان لازم است، تکهکدی است که مقدار علوفهای را محاسبه میکند که هر گاو باید دریافت کند. مهم است که هر گاو مقدار یکسانی دریافت کند؛ باید از بروز درگیری جلوگیری کنید. گاوها بسیار حساس هستند.
خوشبختانه لازم نیست خودتان همهی فرمولهای محاسبهی مقدار علوفه را به دست آورید. شما از یک کتابخانهی بیرونی مرموز استفاده میکنید که آن را در اینترنت پیدا کردهاید. قرار است به شادترین گاوها منجر شود. این کتابخانه کلاسی را در دسترس قرار میدهد که «رابط» زیر را برآورده میکند. در کدی که خودتان مینویسید به این تکیه خواهید کرد.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
همانطور که روی کدتان کار میکنید، مدیریت خطا را بهبود میدهید تا مقاومتر باشد و بعدها که در زندگی روزمرهی مزرعه از آن استفاده میکنید، دیباگ کردنش آسانتر شود.
پیش از هر چیز، روی نوشتن کدی تمرکز میکنید که برای محاسبهی مقدار علوفهی هر گاو لازم است.
متدی به اسم TheFarm.divide_food پیادهسازی کنید که یک شیء FodderCalculator و تعداد گاوها بهصورت یک عدد صحیح را به عنوان آرگومان میپذیرد.
برای این کار، فرض میکنید تعداد گاوها همیشه بزرگتر از صفر است.
این متد باید مقدار غذا به ازای هر گاو را برگرداند.
برای انجام محاسبه، ابتدا باید مقدار کل علوفهی همهی گاوها را به دست آورید.
این کار با فراخوانی متد fodder_amount! و پاس دادن تعداد گاوها انجام میشود.
بهعلاوه، به ضریبی نیاز دارید که این مقدار باید در آن ضرب شود.
این ضریب را با فراخوانی متد fattening_factor! به دست میآورید.
با این دو مقدار و تعداد گاوها، اکنون میتوانید مقدار غذا به ازای هر گاو را محاسبه کنید (بهصورت float64).
اگر یکی از متدهایی که فراخوانی میکنید خطا برگرداند، اجرا باید متوقف شود و 0 برگردانده شود.
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
در حین کار روی تمرین اول، متوجه شدید که کتابخانهی بیرونیای که استفاده میکنید آنقدر که فکر میکردید باکیفیت نیست. برای نمونه، نمیتواند ورودیهای نامعتبر را بهدرستی مدیریت کند. میخواهید این محدودیت را با افزودن یک بررسی برای مقدار ورودی در کد خودتان دور بزنید.
تابعی به اسم TheFarm.validate_input_and_divide_food بنویسید که امضای آن با TheFarm.divide_food بالا یکسان است.
TheFarm.divide_food را فراخوانی کند و نتیجهی آن فراخوانی را برگرداند."Number of cows must be greater than 0" ایجاد کند.TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
بررسی تعداد گاوها پیش از فرستادنش کار خوبی بود، اما از پیام خطای نامشخص چندان راضی نیستید.
تصمیم میگیرید بهتر عمل کنید و یک کلاس خطای سفارشی به اسم InvalidCowsError بسازید.
خطای سفارشی باید تعداد گاوها (Int32) و یک پیام سفارشی (String) را نگه دارد و متد Expection باید دادهها را با قالب زیر سریالایز کند:
{number of cows} cows are invalid: {custom message}
با مجهز بودن به خطای سفارشی خود، تابعی به اسم TheFarm.validate_number_of_cows پیادهسازی کنید که تعداد گاوها را بهصورت یک عدد صحیح میگیرد و یک خطا (یا nil) برمیگرداند.
InvalidCowsError برمیگرداند که پیام سفارشی آن "there are no negative cows" تنظیم شده است.InvalidCowsError برمیگرداند که پیام سفارشی آن "no cows don't need food" تنظیم شده است.nil را برمیگرداند تا نشان دهد اعتبارسنجی موفق بوده است.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
پس از زحمت راهاندازی این تابع اعتبارسنجی، متوجه میشوید که دیگر غروب شده است و میزتان را ترک میکنید تا از تماشای غروب خورشید بر فراز کوهها لذت ببرید. کار افزودن واقعی تابع اعتبارسنجی جدید به کدتان را به روز دیگری موکول میکنید.