مقدمه
در دو درس گذشته، با دو مکانیزم اصلی مدیریت خطا در Rust، یعنی Result<T,
E> برای خطاهای قابل
بازیابی و ماکروی panic! برای خطاهای غیرقابل بازیابی، آشنا شدیم. یک
سوال کلیدی که برای هر
توسعهدهنده Rust پیش میآید این است که "در چه شرایطی باید از Result و در چه شرایطی باید
از
panic! استفاده کنم؟" انتخاب نادرست بین این دو میتواند منجر به کدی
شود که یا بیش از حد
شکننده است یا به صورت نامناسبی خطاهای بحرانی را پنهان میکند.
فلسفه اصلی: خطای قابل انتظار در مقابل باگ
قانون اصلی برای انتخاب بین این دو، به این سوال برمیگردد: "آیا این حالت خطا، یک نتیجهی قابل
انتظار و منطقی از اجرای کد است، یا نشاندهندهی یک وضعیت است که هرگز نباید رخ دهد (یک باگ)؟"
- اگر خطا یک نتیجهی قابل پیشبینی است (مثلاً ورودی نامعتبر از کاربر، عدم وجود یک فایل، یا قطع
شدن شبکه)، شما باید از Result استفاده کنید تا به کد فراخواننده این فرصت را بدهید که
به
صورت کنترلشده با آن خطا برخورد کند.
- اگر خطا نشاندهندهی نقض یک قرارداد یا یک فرض اساسی در منطق برنامه شماست (مثلاً یک مقدار
`null` در جایی که هرگز نباید باشد، یا یک محاسبه ریاضی که منجر به یک حالت نامعتبر میشود)،
panic! کردن انتخاب صحیحتری است. panic به وضوح اعلام
میکند که برنامه وارد یک وضعیت
ناسالم شده و ادامه دادن آن ایمن نیست.
موارد استفادهی مناسب از panic!
هرچند به طور کلی باید از panic! کردن در کدهای کتابخانهای پرهیز
کرد، اما سناریوهایی وجود دارد
که استفاده از آن کاملاً منطقی است.
۱. نمونهسازی سریع و کدهای مثال
وقتی در حال نوشتن یک کد مثال یا نمونهسازی یک ایده هستید، ممکن است نخواهید زمان زیادی را صرف
مدیریت خطای کامل کنید. در این موارد، استفاده از متدهایی مانند unwrap یا expect (که
در صورت بروز خطا panic میکنند) میتواند به شما کمک کند تا سریعتر روی منطق اصلی تمرکز
کنید.
src/main.rs
use std::net::IpAddr;
fn main() {
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
در این مثال، ما مطمئن هستیم که آدرس IP که به صورت ثابت در کد وارد شده، معتبر است. بنابراین،
استفاده از expect منطقی است. اگر این آدرس نامعتبر بود، این یک باگ در خود کد ماست و
panic کردن رفتار درستی است.
۲. زمانی که ادامه دادن منطقی نیست
گاهی اوقات، یک خطا برنامه شما را در وضعیتی قرار میدهد که هیچ راه منطقی برای بازیابی آن وجود
ندارد. اگر کد شما به یک حالت "بد" برسد که در آن، هر عملیاتی که انجام دهد احتمالاً اشتباه خواهد
بود یا دادهها را خراب خواهد کرد، بهتر است فوراً متوقف شود.
۳. خطاهایی که کد فراخواننده نباید مدیریت کند
اگر تابعی مینویسید و نقض یک پیششرط توسط کد فراخواننده، تنها راه بروز یک خطا است، panic
کردن
میتواند مناسب باشد. در این حالت، panic کردن به وضوح نشان میدهد که یک باگ در کد
فراخواننده وجود دارد.
ایجاد نوعهای سفارشی برای اعتبارسنجی
یک الگوی رایج و قدرتمند در Rust برای جلوگیری از خطاهای منطقی، استفاده از سیستم نوع (type system)
برای اعتبارسنجی دادهها در زمان ساخت است. به جای اینکه دادهها را در همه جا بررسی کنید،
میتوانید یک نوع جدید بسازید که تنها از طریق یک تابع سازنده قابل ایجاد باشد و این تابع، قوانین
اعتبارسنجی را اعمال کند.
src/lib.rs
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
در این مثال از بازی حدس عدد، ما یک نوع جدید به نام Guess ساختهایم. تنها راه ساخت یک
نمونه از
Guess، استفاده از تابع Guess::new() است. این تابع بررسی
میکند که
مقدار ورودی بین ۱ تا ۱۰۰ باشد. اگر نباشد، panic میکند. این کار تضمین میکند که هر نمونه
از
Guess که در برنامه ما وجود دارد، حتماً یک مقدار معتبر را در خود جای داده است و توابع
دیگری که
یک Guess را به عنوان ورودی میگیرند، دیگر نیازی به بررسی مجدد این شرط ندارند.
در این درس به بررسی تفاوتهای فلسفی و کاربردی بین Result و panic! پرداختیم. یاد گرفتیم که
Result برای خطاهای قابل انتظار و panic! برای باگها و خطاهای
غیرقابل بازیابی به کار میرود.
با این درس، فصل «مدیریت خطا در Rust» به پایان میرسد. ما اکنون درک عمیقی از رویکرد قوی Rust به
مدیریت خطا داریم که یکی از دلایل اصلی شهرت این زبان به قابلیت اطمینان است. در فصل بعدی، به سراغ
مفاهیم قدرتمندی مانند «نوعهای جنریک، Traitها و Lifetimeها» خواهیم رفت که به ما اجازه میدهند
کدهای انتزاعی، قابل استفاده مجدد و بسیار کارآمدی بنویسیم.