برنامهنویسان معمولاً تلاش میکنند نرمافزاری بیعیب بنویسند و معمولاً موفق نمیشوند.
کارها به شکلی غیرمنتظره اشتباه پیش میروند و ما باید بتوانیم از پسشان بربیاییم.
بعضی از طراحان زبان معتقدند که اولویت این است هرچه سریعتر خطایی را تشخیص دهیم و بعد اجرا را با پیامی روشنگر متوقف کنیم تا دیباگکردن آسانتر شود.
زبانهای علم داده معمولاً رویکرد ظریفتری در پیش میگیرند. بعضی خطاها آنقدر جدیاند که توقف فوری لازم است، اما اغلب بهتر است مشکل را علامت بزنیم تا بعداً به آن پرداخته شود و سپس اجرا را ادامه دهیم.
در مفهوم نیستی دیدیم که جولیا جایگزینهای گوناگونی برای مقادیر مشکلدار فراهم میکند: missing، NaN و Inf. اینکه اینها در موقعیتی خاص رویکرد بهتری از توقف برنامه باشند، به قضاوت برنامهنویس بستگی دارد.
یک نکته دربارهی نامگذاری پیش از ورود به جزئیات: مستندات جولیا دو واژهی «error» و «exception» را تا حد زیادی هممعنا در نظر میگیرد. متن زیر هم ممکن است به همان اندازه ناهماهنگ باشد.
تا اینجای برنامهی درسی، حتماً پیامهای خطای زیادی از جولیا دیدهاید. برای نمونه:
julia> Int(3.14)
ERROR: InexactError: Int64(3.14)
تبدیل یک عدد اعشاری به عدد صحیح با از دست رفتن دقت همراه است، بنابراین InexactError میگیریم.
InexactError یک نوع است، یکی از چندین نوعی (در حال حاضر ۲۵) که بهصورت استاندارد در جولیا تعبیه شدهاند. همهی آنها زیرنوع Exception هستند:
julia> supertype(InexactError)
Exception
throw()ممکن است تولید بعضی از انواع خطای استاندارد در کد خودتان به کار بیاید.
مثل همهی انواع مشخص، خطاها هم سازنده دارند. آرگومانهای گوناگونی میگیرند، پس برای نوعی که میخواهید استفاده کنید، مستندات را ببینید.
julia> DomainError(42, "out of range")
DomainError(42, "out of range")
برای استفاده از خطا، سازنده را در یک تابع throw() بپیچید:
julia> throw(DomainError(42, "out of range"))
ERROR: DomainError with 42:
out of range
error()برای یک راه سریع و بیدردسر، تابع error() میتواند مناسب باشد. یک رشته (یا اجزای یک رشته) را بهعنوان آرگومان میگیرد:
julia> happy = false;
julia> happy || error("😞 something went wrong")
ERROR: 😞 something went wrong
ساختن انواع خطای جدید در اصل بسیار آسان است. کافی است زیرنوع دیگری از Exception اضافه کنید:
julia> struct MyError <: Exception end
julia> throw(MyError)
ERROR: MyError
ایدهی اصلی یک «ادعا» این است: «این گزاره باید درست باشد، پس اگر نادرست است، بلند اعتراض کنید.» ارزش این کار بیشتر هنگام دیباگکردن است، چون در کد تولیدی هرگز نباید ادعایی شکست بخورد.
در مفهوم نوع دیدیم که میتوانیم ادعای نوع اضافه کنیم، مثلاً برای بررسی نوع بازگشتی یک تابع.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
بهطور کلیتر، «ماکروی» @assert به ما اجازه میدهد هر عبارتی را که به یک مقدار «منطقی» ارزیابی میشود بیازماییم:
julia> n = 22;
julia> @assert isodd(n) "n must be odd"
ERROR: AssertionError: n must be odd
try...catch
بعضی خطاها ناگزیر کشندهاند، اما اغلب انتظار داریم برنامه بهسلامت از خطا عبور کند.
بهطور پیشفرض، یک خطا فوراً تابع جاری را متوقف میکند و خطا (همراه با هر پیام آگاهیبخش) به تابع فراخواننده داده میشود.
این روند در «پشتهی فراخوانی» به بالا ادامه مییابد تا اینکه کد سطح بالا با یک پیام خطا متوقف شود.
در هر مرحله، میتوان خطا را با یک بلوک try...catch که تلاش میکند آن را مدیریت کند، رهگیری کرد.
julia> n = -1;
julia> try
log_n = log(n)
catch problem
if problem isa DomainError # number out of range
# See next section for more on @warn and @info
@warn "you may have supplied a negative real number: $n"
@info "trying with complex argument"
log_n = log(Complex(n)) # fallback calculation
elseif problem isa MethodError # no idea what n is
@error "please supply a valid argument"
else
rethrow() # the error could be anything else
end
end
┌ Warning: you may have supplied a negative real number: -1
└ @ Main REPL[3]:5
[ Info: trying with complex argument
0.0 + 3.141592653589793im # success
در مثال بالا، log(n) نیاز دارد که n یا یک مقدار حقیقی مثبت باشد یا هر مقدار مختلطی. ساختار try ... catch مشکلات مربوط به مقادیر حقیقی منفی را میگیرد و پاسخ مختلط درست، یعنی iπ در نمادگذاری ریاضی، را برمیگرداند.
اگر مثلاً یک آرگومان رشتهای بدهید، راهی برای بازیابی نیست جز اینکه از کاربر بخواهید آن را تصحیح کند.
بهعنوان یک راهحل نهایی همهشمول، rethrow() را برای هر چیزی که نه DomainError باشد و نه MethodError اضافه کردیم.
توجه: گاهی try...catch همان چیزی است که لازم دارید، اما لطفاً از استفادهی بیش از حد آن بپرهیزید. اگر بتوان بهجای آن از بلوک if...else استفاده کرد، عملکردش بسیار بهتر از گرفتن استثناها خواهد بود.
توجه داشته باشید که تابع error() که بالاتر بحث شد، نباید با ماکروی @error اشتباه گرفته شود.
این تابع یک استثنا تولید میکند که اگر گرفته نشود، در پشتهی فراخوانی به بالا منتقل میشود.
ماکروی @error همراه با همتایانش @debug، @info و @warn، بخشی از ماژول Logging است و برای تولید پیامهای آگاهیبخش بدون تغییر دادن جریان برنامه در نظر گرفته شده است.
خروجی بهطور پیشفرض به ترمینال میرود (بر اساس شدت رنگبندی میشود)، هرچند در یک برنامهی واقعی احتمالات دیگری هم وجود دارد.
julia> @warn "Something looks not quite right"
┌ Warning: Something looks not quite right
└ @ Main REPL[55]:1
julia> @error "Panic!"
┌ Error: Panic!
└ @ Main REPL[56]:1
مثال قبلی را هم در بخش try...catch ببینید.
النا مدیر کیفیت جدید یک کارخانهی روزنامه است. چون تازه به شرکت آمده، تصمیم گرفته برخی از فرایندهای کارخانه را بررسی کند تا ببیند چه چیزهایی را میتوان بهتر کرد. او پی برده که تکنسینها بسیاری از بررسیهای کیفیت را دستی انجام میدهند. او فرصت خوبی برای خودکارسازی میبیند و از شما، که یک توسعهدهندهی آزاد هستید، میخواهد نرمافزاری بنویسید تا برخی از دستگاهها را پایش کند.
اولین مأموریت شما نوشتن نرمافزاری است که سطح رطوبت اتاق تولید را پایش کند. در حال حاضر حسگری به نرمافزار شرکت متصل است که بهطور دورهای درصد رطوبت اتاق را برمیگرداند.
شما باید تابعی در نرمافزار پیادهسازی کنید که اگر درصد رطوبت بیش از حد بالا باشد، خطایی پرتاب کند.
اگر رطوبت در سطح قابل قبولی باشد، یک لاگ Info اضافه میشود.
اسم این تابع باید humiditycheck باشد و درصد رطوبت را به عنوان آرگومان بگیرد.
اگر این درصد از ۷۰٪ فراتر رود، باید با یک ErrorException متوقف شوید (پیام دقیق مهم نیست، اما باید سطح رطوبت اندازهگیریشده را در خود داشته باشد).
در غیر این صورت، یک لاگ Info با پیام "humidity level check passed: h%" اضافه کنید که در آن h درصد رطوبت است.
julia> humiditycheck(60)
[ Info: humidity level check passed: 60%
julia> humiditycheck(100)
ERROR: humidity check failed: 100%
النا از اولین مأموریت شما بسیار راضی است و از شما میخواهد که به پایش دمای دستگاهها بپردازید. وقتی با یک تکنسین به اسم گرگ گپ میزنید، به شما گفته میشود که اگر دمای یک دستگاه از ۵۰۰ درجه سلسیوس فراتر رود، تکنسینها نگران گرمای بیش از حد میشوند.
دستگاه به حسگری مجهز است که دمای داخلی آن را اندازه میگیرد. باید بدانید که این حسگر بسیار حساس است و اغلب خراب میشود. در این صورت، تکنسینها باید آن را عوض کنند.
وظیفهی شما پیادهسازی تابعی به اسم temperaturecheck است که دما را به عنوان آرگومان میگیرد و اگر همهچیز خوب باشد لاگی اضافه میکند، و اگر حسگر خراب باشد یا دستگاه شروع به گرمشدن بیش از حد کند، خطایی پرتاب میکند.
از آنجا که بعداً باید بسته به نوع خطا واکنش متفاوتی نشان دهید، به سازوکاری نیاز دارید که این دو نوع خطا را از هم تشخیص دهد.
nothing خواهد بود.
در این صورت، باید با یک ArgumentError متوقف شوید (پیام مهم نیست).DomainError پرتاب کنید که دمای اندازهگیریشده را در خود داشته باشد."temperature check passed: t °C" اضافه کنید که در آن t دماست.julia> temperaturecheck(nothing)
ERROR: ArgumentError: sensor is broken
julia> temperaturecheck(800)
ERROR: DomainError with 800:
"overheating detected"
julia> temperaturecheck(500)
[ Info: temperature check passed: 500 °C
برای مأموریت بعدی، باید یک خطای عمومیتر و فراگیر تعریف کنید.
جزئیات پیادهسازی مهم نیست؛ تنها نکتهی مهم این است که یک خطا باشد و اسمش MachineError باشد.
میتوانید هر فیلد و پیامی را که به نظرتان مفید است در آن بگنجانید.
حالا که دستگاه شما میتواند خطاها را تشخیص دهد و یک خطای سفارشی دستگاه هم دارید، تابعی پوششی اضافه میکنید که میتواند گزارش دهد همهچیز چطور کار میکند. این تابع پوششی، علاوه بر برگرداندن لاگهای توابع قبلی، باید بسته به نوع (یا انواع) شکستهایی که رخ میدهد لاگهایی هم اضافه کند.
ErrorException پرتاب کند، باید یک لاگ Error با پیام "humidity level check failed: h%" اضافه شود که در آن h درصد رطوبت است.ArgumentError پرتاب کند، باید یک لاگ Warn با پیام "sensor is broken" اضافه شود.DomainError پرتاب کند، باید یک لاگ Error با پیام "overheating detected: t °C" اضافه شود که در آن t دماست.MachineError واحد پرتاب شود.humiditycheck و temperaturecheck اضافه میشوند.تابعی به اسم machinemonitor() پیادهسازی کنید که رطوبت و دما را به عنوان آرگومان میگیرد.
julia> machinemonitor(42, 450)
[ Info: humidity level check passed: 42%
[ Info: temperature check passed: 450 °C
julia> machinemonitor(42, 550)
[ Info: humidity level check passed: 42%
┌ Error: overheating detected: 550 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(82, 521)
┌ Error: humidity level check failed: 82%
└ @ Main # output truncated
┌ Error: overheating detected: 521 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(42, nothing)
[ Info: humidity level check passed: 42%
┌ Warning: sensor is broken
└ @ Main # output truncated
Error: MachineError