آیا باید از panic!
استفاده کنیم یا نه؟
چگونه تصمیم میگیرید که چه زمانی باید panic!
را فراخوانی کنید و چه زمانی باید یک Result
بازگردانید؟ وقتی کد دچار خطا میشود، هیچ راهی برای بازیابی وجود ندارد. شما میتوانید در هر وضعیت خطایی، چه قابل بازیابی باشد و چه نباشد، panic!
را فراخوانی کنید، اما در این صورت، شما به جای کد فراخوانیکننده تصمیم میگیرید که وضعیت غیرقابل بازیابی است. وقتی تصمیم میگیرید یک مقدار Result
بازگردانید، به کد فراخوانیکننده گزینههایی میدهید. کد فراخوانیکننده میتواند انتخاب کند که تلاش کند خطا را به روشی که برای وضعیت خودش مناسب است بازیابی کند، یا میتواند تصمیم بگیرد که مقدار Err
در این مورد غیرقابل بازیابی است و بنابراین panic!
را فراخوانی کرده و خطای قابل بازیابی شما را به یک خطای غیرقابل بازیابی تبدیل کند. بنابراین، بازگرداندن Result
یک انتخاب پیشفرض خوب است وقتی تابعی تعریف میکنید که ممکن است شکست بخورد.
در وضعیتهایی مانند مثالها، کد نمونهسازی (prototype) و آزمونها، مناسبتر است که کدی بنویسید که متوقف شود به جای بازگرداندن یک Result
. بیایید بررسی کنیم چرا، سپس وضعیتهایی را بحث کنیم که کامپایلر نمیتواند بفهمد که شکست غیرممکن است، اما شما به عنوان یک انسان میتوانید. این فصل با برخی دستورالعملهای کلی درباره تصمیمگیری درباره اینکه آیا در کد کتابخانه باید از panic!
استفاده کرد یا نه، به پایان خواهد رسید.
مثالها، کد نمونهسازی، و آزمونها
وقتی مثالی مینویسید تا یک مفهوم را توضیح دهید، همچنین افزودن کد مدیریت خطای قدرتمند میتواند مثال را کمتر واضح کند. در مثالها، این نکته فهمیده میشود که فراخوانی به متدی مانند unwrap
که ممکن است متوقف شود، به عنوان یک جایگزین برای روشی که میخواهید برنامه شما خطاها را مدیریت کند در نظر گرفته میشود، که میتواند بسته به آنچه بقیه کد شما انجام میدهد متفاوت باشد.
به همین ترتیب، متدهای unwrap
و expect
بسیار مفید هستند وقتی که در حال نمونهسازی هستید و هنوز تصمیم نگرفتهاید که چگونه خطاها را مدیریت کنید. آنها نشانههای واضحی در کد شما میگذارند برای زمانی که آماده باشید برنامه خود را قدرتمندتر کنید.
اگر یک متد در یک آزمون شکست بخورد، میخواهید کل آزمون شکست بخورد، حتی اگر آن متد ویژگیای که تحت آزمون قرار دارد نباشد. از آنجا که panic!
راهی است که یک آزمون به عنوان شکستخورده علامتگذاری میشود، فراخوانی unwrap
یا expect
دقیقاً همان چیزی است که باید اتفاق بیفتد.
مواردی که شما اطلاعات بیشتری نسبت به کامپایلر دارید
در زمانی که منطق دیگری در برنامه شما وجود دارد که تضمین میکند مقدار Result
از نوع Ok
خواهد بود،
اما این منطق چیزی نیست که کامپایلر بتواند آن را درک کند،
استفاده از expect
نیز مناسب خواهد بود.
شما همچنان با یک مقدار Result
مواجه هستید که باید آن را مدیریت کنید:
عملیاتی که فراخوانی میکنید بهصورت کلی ممکن است شکست بخورد،
حتی اگر در موقعیت خاص شما از نظر منطقی وقوع خطا غیرممکن باشد.
اگر با بررسی دستی کد بتوانید اطمینان حاصل کنید که هیچگاه با واریانت Err
روبهرو نخواهید شد،
استفاده از expect
کاملاً قابلقبول است،
به شرط آنکه دلیل این اطمینان خود را در قالب متن آرگومان expect
مستندسازی کنید.
در اینجا یک مثال آورده شده است:
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); }
ما یک نمونه IpAddr
را با تجزیه یک رشته ثابتشده ایجاد میکنیم. ما میتوانیم ببینیم که 127.0.0.1
یک آدرس IP معتبر است، بنابراین استفاده از expect
در اینجا قابل قبول است. با این حال، داشتن یک رشته ثابتشده و معتبر نوع بازگشتی متد parse
را تغییر نمیدهد: ما همچنان یک مقدار Result
دریافت میکنیم و کامپایلر همچنان ما را مجبور میکند که با Result
برخورد کنیم، انگار که حالت Err
ممکن است، زیرا کامپایلر به اندازه کافی هوشمند نیست تا ببیند این رشته همیشه یک آدرس IP معتبر است. اگر رشته آدرس IP از یک کاربر میآمد به جای اینکه در برنامه ثابت شده باشد و بنابراین امکان شکست وجود داشت، قطعاً میخواستیم که Result
را به روشی قدرتمندتر مدیریت کنیم. اشاره به این فرض که این آدرس IP ثابتشده است، ما را ترغیب میکند که در صورت نیاز به دریافت آدرس IP از منبع دیگری در آینده، expect
را به کد مدیریت خطای بهتر تغییر دهیم.
دستورالعملهایی برای مدیریت خطاها
توصیه میشود که کد شما زمانی که ممکن است به وضعیت نامناسبی برسد، دچار panic!
شود. در این زمینه، یک وضعیت نامناسب زمانی رخ میدهد که برخی فرضیات، تضمینها، قراردادها، یا تغییرناپذیریها شکسته شوند، مانند زمانی که مقادیر نامعتبر، مقادیر متناقض، یا مقادیر گمشده به کد شما پاس داده میشوند—به علاوه یکی یا بیشتر از شرایط زیر:
- وضعیت نادرست (bad state) چیزی غیرمنتظره است، بر خلاف موقعیتهایی که احتمالاً گاهی اتفاق میافتند، مانند وارد کردن داده با فرمت نادرست توسط کاربر.
- کد شما پس از این نقطه باید به نبودن در چنین وضعیت نادرستی تکیه کند، بهجای آنکه در هر مرحله مشکل را بررسی کند.
- راه مناسبی برای رمزگذاری این اطلاعات در قالب typeهایی که استفاده میکنید وجود ندارد. در فصل ۱۸، در بخش “رمزگذاری وضعیتها و رفتارها بهصورت type” با مثالی منظور خود را توضیح خواهیم داد.
اگر کسی کد شما را فراخوانی کند و مقادیری که منطقی نیستند را پاس دهد، بهتر است که یک خطا بازگردانید تا کاربر کتابخانه بتواند تصمیم بگیرد که در آن مورد چه کاری انجام دهد. با این حال، در مواردی که ادامه دادن میتواند ناامن یا مضر باشد، بهترین انتخاب ممکن است فراخوانی panic!
و هشدار به شخصی که از کتابخانه شما استفاده میکند درباره باگ در کد آنها باشد تا بتوانند آن را در حین توسعه رفع کنند. به همین ترتیب، panic!
اغلب مناسب است اگر کد خارجی که از کنترل شما خارج است را فراخوانی میکنید و آن کد یک وضعیت نامعتبر بازمیگرداند که شما هیچ راهی برای رفع آن ندارید.
با این حال، زمانی که شکست مورد انتظار است، مناسبتر است که یک Result
بازگردانید تا یک فراخوانی panic!
. مثالها شامل پردازشی هستند که دادههای نادرست دریافت میکند یا یک درخواست HTTP که بازگشت وضعیت نشان میدهد که به محدودیت نرخ برخورد کردهاید. در این موارد، بازگرداندن یک Result
نشان میدهد که شکست یک احتمال مورد انتظار است که کد فراخوانیکننده باید تصمیم بگیرد چگونه آن را مدیریت کند.
وقتی کد شما عملیاتی انجام میدهد که میتواند در صورت فراخوانی با مقادیر نامعتبر کاربر را در معرض خطر قرار دهد، کد شما باید ابتدا مقادیر را تأیید کند و اگر مقادیر نامعتبر هستند دچار panic!
شود. این بیشتر به دلایل ایمنی است: تلاش برای انجام عملیات روی دادههای نامعتبر میتواند کد شما را در معرض آسیبپذیریها قرار دهد. این دلیل اصلی است که کتابخانه استاندارد اگر شما تلاش کنید به حافظه خارج از محدوده دسترسی پیدا کنید، دچار panic!
میشود: تلاش برای دسترسی به حافظهای که به ساختار داده جاری تعلق ندارد یک مشکل امنیتی رایج است. توابع اغلب قراردادهایی دارند: رفتار آنها فقط در صورتی تضمین میشود که ورودیها نیازمندیهای خاصی را برآورده کنند. دچار panic!
شدن وقتی که قرارداد نقض میشود منطقی است زیرا نقض قرارداد همیشه نشاندهنده یک باگ در طرف فراخوانیکننده است و نوع خطایی نیست که بخواهید کد فراخوانیکننده به طور صریح مدیریت کند. در واقع، هیچ راه معقولی برای بازیابی کد فراخوانیکننده وجود ندارد؛ برنامهنویسان فراخوانیکننده باید کد را اصلاح کنند. قراردادهای یک تابع، به خصوص زمانی که نقض آن باعث panic!
میشود، باید در مستندات API تابع توضیح داده شوند.
با این حال، داشتن بررسیهای خطا در تمام توابع شما بسیار طولانی و ناخوشایند خواهد بود. خوشبختانه، شما میتوانید از سیستم نوع Rust (و در نتیجه بررسی نوعی که توسط کامپایلر انجام میشود) برای انجام بسیاری از بررسیها استفاده کنید. اگر تابع شما یک نوع خاص را به عنوان پارامتر داشته باشد، میتوانید با اطمینان از اینکه کامپایلر قبلاً تضمین کرده است که یک مقدار معتبر دارید، منطق کد خود را پیش ببرید. برای مثال، اگر شما یک نوع به جای یک Option
داشته باشید، برنامه شما انتظار دارد که چیزی به جای هیچچیز وجود داشته باشد. سپس کد شما نیازی به مدیریت دو حالت برای حالتهای Some
و None
ندارد: فقط یک حالت برای داشتن یک مقدار به طور قطعی خواهد داشت. کدی که سعی میکند هیچچیز به تابع شما پاس دهد حتی کامپایل نخواهد شد، بنابراین تابع شما نیازی به بررسی این حالت در زمان اجرا ندارد. مثال دیگر استفاده از یک نوع عددی بدون علامت مانند u32
است که تضمین میکند پارامتر هرگز منفی نخواهد بود.
ایجاد انواع سفارشی برای اعتبارسنجی
بیایید ایده استفاده از سیستم نوع Rust برای اطمینان از داشتن یک مقدار معتبر را یک قدم فراتر ببریم و به ایجاد یک نوع سفارشی برای اعتبارسنجی نگاه کنیم. بازی حدس عدد در فصل ۲ را به یاد بیاورید که کد ما از کاربر خواست تا یک عدد بین ۱ تا ۱۰۰ حدس بزند. ما هرگز اعتبارسنجی نکردیم که حدس کاربر بین این اعداد باشد قبل از اینکه آن را با عدد مخفی مقایسه کنیم؛ فقط بررسی کردیم که حدس مثبت باشد. در این مورد، پیامدها چندان شدید نبودند: خروجی ما با پیامهای “خیلی بزرگ” یا “خیلی کوچک” همچنان درست بود. اما این میتواند بهبودی مفید باشد که کاربر را به سمت حدسهای معتبر هدایت کنیم و رفتار متفاوتی داشته باشیم وقتی کاربر عددی خارج از محدوده حدس میزند در مقابل زمانی که، برای مثال، حروف تایپ میکند.
یک راه برای انجام این کار این است که حدس را به جای فقط یک u32
، به صورت یک i32
تجزیه کنیم تا اجازه دهیم اعداد منفی نیز در نظر گرفته شوند، و سپس یک بررسی برای اینکه عدد در محدوده است یا نه اضافه کنیم، مانند زیر:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
عبارت if
بررسی میکند که آیا مقدار ما خارج از محدوده است، به کاربر درباره مشکل اطلاع میدهد و continue
را فراخوانی میکند تا تکرار بعدی حلقه شروع شود و درخواست یک حدس دیگر شود. بعد از عبارت if
، میتوانیم با مقایسه بین guess
و عدد مخفی ادامه دهیم، زیرا میدانیم که guess
بین ۱ و ۱۰۰ است.
با این حال، این یک راهحل ایدهآل نیست: اگر بسیار حیاتی باشد که برنامه فقط بر روی مقادیر بین ۱ و ۱۰۰ عمل کند، و برنامه توابع زیادی با این نیاز داشته باشد، داشتن چنین بررسیهایی در هر تابع خستهکننده خواهد بود (و ممکن است عملکرد را تحت تأثیر قرار دهد).
در عوض، میتوانیم یک نوع جدید در یک ماژول اختصاصی تعریف کنیم
و اعتبارسنجیها (validations) را در تابعی قرار دهیم که وظیفهی ایجاد یک نمونه از آن نوع را دارد،
بهجای آنکه این اعتبارسنجیها را در همهجا تکرار کنیم.
به این ترتیب، استفاده از این نوع جدید در امضای توابع ایمن خواهد بود
و میتوان با اطمینان از مقادیری که دریافت میکنند استفاده کرد.
لیستینگ 9-13 یک روش برای تعریف نوع Guess
را نشان میدهد
که تنها زمانی یک نمونه از Guess
ایجاد میکند که تابع new
مقداری بین 1 تا 100 دریافت کرده باشد.
#![allow(unused)] fn main() { 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
که تنها با مقادیری بین 1 تا 100 ادامه میدهدتوجه داشته باشید که این کد در فایل src/guessing_game.rs
بستگی به اضافه کردن یک اعلان ماژول به شکل mod guessing_game;
در فایل src/lib.rs دارد
که در اینجا نشان داده نشده است.
درون فایل این ماژول جدید، یک struct
به نام Guess
تعریف کردهایم
که در همان ماژول قرار دارد و دارای یک فیلد به نام value
است که یک مقدار از نوع i32
را نگه میدارد.
این همان مکانی است که عدد در آن ذخیره خواهد شد.
سپس یک تابع وابسته به نام new
روی Guess
پیادهسازی میکنیم که نمونههایی از مقادیر Guess
ایجاد میکند. تابع new
به گونهای تعریف شده که یک پارامتر به نام value
از نوع i32
داشته باشد و یک Guess
بازگرداند. کدی که در بدنه تابع new
قرار دارد مقدار value
را بررسی میکند تا مطمئن شود که بین ۱ و ۱۰۰ است. اگر مقدار value
این آزمون را پاس نکند، یک فراخوانی به panic!
انجام میدهیم، که به برنامهنویسی که کد فراخوانیکننده را مینویسد هشدار میدهد که باگی دارد که باید برطرف کند، زیرا ایجاد یک Guess
با مقدار value
خارج از این محدوده قرارداد تابع Guess::new
را نقض میکند. شرایطی که ممکن است باعث panic!
در Guess::new
شود باید در مستندات عمومی API آن مورد بحث قرار گیرد؛ ما در فصل ۱۴ درباره قراردادهای مستندات که نشاندهنده احتمال وقوع panic!
هستند صحبت خواهیم کرد. اگر مقدار value
آزمون را پاس کند، یک Guess
جدید با فیلد value
تنظیم شده به پارامتر value
ایجاد میکنیم و Guess
را بازمیگردانیم.
در مرحلهی بعد، متدی به نام value
پیادهسازی میکنیم که self
را بهصورت رفرنس قرض میگیرد،
پارامتر دیگری ندارد، و یک مقدار i32
بازمیگرداند.
این نوع متد گاهی getter نامیده میشود، زیرا هدف آن دریافت دادهای از فیلدهای ساختار و بازگرداندن آن است.
این متد عمومی لازم است چون فیلد value
در struct
مربوط به Guess
خصوصی است.
خصوصی بودن فیلد value
اهمیت دارد تا کدی که از struct
Guess
استفاده میکند،
اجازه نداشته باشد مستقیماً value
را مقداردهی کند:
کد خارج از ماژول guessing_game
باید از تابع Guess::new
برای ساخت نمونهای از Guess
استفاده کند،
و به این ترتیب، اطمینان حاصل میشود که هیچ راهی برای ساخت یک Guess
با مقدار value
ای
که بررسیهای موجود در تابع Guess::new
را نگذرانده، وجود ندارد.
تابعی که یک پارامتر میگیرد یا فقط اعدادی بین ۱ و ۱۰۰ بازمیگرداند میتواند در امضای خود اعلام کند که یک Guess
میگیرد یا بازمیگرداند به جای یک i32
و نیازی به انجام بررسیهای اضافی در بدنه خود ندارد.
خلاصه
ویژگیهای مدیریت خطای Rust طراحی شدهاند تا به شما کمک کنند کدی قدرتمندتر بنویسید. ماکروی panic!
نشان میدهد که برنامه شما در حالتی قرار دارد که نمیتواند آن را مدیریت کند و به شما امکان میدهد فرآیند را متوقف کنید به جای اینکه سعی کنید با مقادیر نامعتبر یا نادرست ادامه دهید. Enum Result
از سیستم نوع Rust استفاده میکند تا نشان دهد که عملیات ممکن است به روشی شکست بخورد که کد شما میتواند از آن بازیابی کند. میتوانید از Result
برای اطلاع دادن به کدی که کد شما را فراخوانی میکند استفاده کنید که باید موفقیت یا شکست احتمالی را نیز مدیریت کند. استفاده از panic!
و Result
در شرایط مناسب باعث میشود کد شما در برابر مشکلات اجتنابناپذیر قابل اطمینانتر شود.
حالا که راههای مفید استفاده کتابخانه استاندارد از جنریکها با Enums Option
و Result
را دیدهاید، درباره نحوه عملکرد جنریکها و نحوه استفاده از آنها در کد خود صحبت خواهیم کرد.