آیا باید از panic!
استفاده کنیم یا نه؟
چگونه تصمیم میگیرید که چه زمانی باید panic!
را فراخوانی کنید و چه زمانی باید یک Result
بازگردانید؟ وقتی کد دچار خطا میشود، هیچ راهی برای بازیابی وجود ندارد. شما میتوانید در هر وضعیت خطایی، چه قابل بازیابی باشد و چه نباشد، panic!
را فراخوانی کنید، اما در این صورت، شما به جای کد فراخوانیکننده تصمیم میگیرید که وضعیت غیرقابل بازیابی است. وقتی تصمیم میگیرید یک مقدار Result
بازگردانید، به کد فراخوانیکننده گزینههایی میدهید. کد فراخوانیکننده میتواند انتخاب کند که تلاش کند خطا را به روشی که برای وضعیت خودش مناسب است بازیابی کند، یا میتواند تصمیم بگیرد که مقدار Err
در این مورد غیرقابل بازیابی است و بنابراین panic!
را فراخوانی کرده و خطای قابل بازیابی شما را به یک خطای غیرقابل بازیابی تبدیل کند. بنابراین، بازگرداندن Result
یک انتخاب پیشفرض خوب است وقتی تابعی تعریف میکنید که ممکن است شکست بخورد.
در وضعیتهایی مانند مثالها، کد نمونهسازی (prototype) و آزمونها، مناسبتر است که کدی بنویسید که متوقف شود به جای بازگرداندن یک Result
. بیایید بررسی کنیم چرا، سپس وضعیتهایی را بحث کنیم که کامپایلر نمیتواند بفهمد که شکست غیرممکن است، اما شما به عنوان یک انسان میتوانید. این فصل با برخی دستورالعملهای کلی درباره تصمیمگیری درباره اینکه آیا در کد کتابخانه باید از panic!
استفاده کرد یا نه، به پایان خواهد رسید.
مثالها، کد نمونهسازی، و آزمونها
وقتی مثالی مینویسید تا یک مفهوم را توضیح دهید، همچنین افزودن کد مدیریت خطای قدرتمند میتواند مثال را کمتر واضح کند. در مثالها، این نکته فهمیده میشود که فراخوانی به متدی مانند unwrap
که ممکن است متوقف شود، به عنوان یک جایگزین برای روشی که میخواهید برنامه شما خطاها را مدیریت کند در نظر گرفته میشود، که میتواند بسته به آنچه بقیه کد شما انجام میدهد متفاوت باشد.
به همین ترتیب، متدهای unwrap
و expect
بسیار مفید هستند وقتی که در حال نمونهسازی هستید و هنوز تصمیم نگرفتهاید که چگونه خطاها را مدیریت کنید. آنها نشانههای واضحی در کد شما میگذارند برای زمانی که آماده باشید برنامه خود را قدرتمندتر کنید.
اگر یک متد در یک آزمون شکست بخورد، میخواهید کل آزمون شکست بخورد، حتی اگر آن متد ویژگیای که تحت آزمون قرار دارد نباشد. از آنجا که panic!
راهی است که یک آزمون به عنوان شکستخورده علامتگذاری میشود، فراخوانی unwrap
یا expect
دقیقاً همان چیزی است که باید اتفاق بیفتد.
مواردی که شما اطلاعات بیشتری نسبت به کامپایلر دارید
همچنین مناسب است که unwrap
یا expect
را فراخوانی کنید وقتی منطق دیگری دارید که تضمین میکند مقدار Result
دارای یک مقدار Ok
خواهد بود، اما این منطق چیزی نیست که کامپایلر آن را بفهمد. شما همچنان یک مقدار Result
دارید که باید مدیریت کنید: عملیاتی که فراخوانی میکنید همچنان امکان شکست خوردن دارد، حتی اگر به صورت منطقی در وضعیت خاص شما غیرممکن باشد. اگر میتوانید با بازرسی دستی کد تضمین کنید که هرگز یک حالت Err
نخواهید داشت، کاملاً قابل قبول است که unwrap
را فراخوانی کنید و حتی بهتر است که دلیل خود را در متن 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!
شود. در این زمینه، یک وضعیت نامناسب زمانی رخ میدهد که برخی فرضیات، تضمینها، قراردادها، یا تغییرناپذیریها شکسته شوند، مانند زمانی که مقادیر نامعتبر، مقادیر متناقض، یا مقادیر گمشده به کد شما پاس داده میشوند—به علاوه یکی یا بیشتر از شرایط زیر:
- وضعیت نامناسب چیزی غیرمنتظره است، بر خلاف چیزی که احتمالاً گهگاهی رخ میدهد، مانند کاربری که دادهها را در قالب اشتباه وارد میکند.
- کد شما پس از این نقطه نیاز دارد که به عدم وجود در این وضعیت نامناسب تکیه کند، به جای اینکه مشکل را در هر مرحله بررسی کند.
- راه مناسبی برای رمزگذاری این اطلاعات در نوعهایی که استفاده میکنید وجود ندارد. ما در بخش “رمزگذاری وضعیتها و رفتار به عنوان نوعها” در فصل ۱۸ یک مثال از آنچه که منظورمان است را بررسی خواهیم کرد.
اگر کسی کد شما را فراخوانی کند و مقادیری که منطقی نیستند را پاس دهد، بهتر است که یک خطا بازگردانید تا کاربر کتابخانه بتواند تصمیم بگیرد که در آن مورد چه کاری انجام دهد. با این حال، در مواردی که ادامه دادن میتواند ناامن یا مضر باشد، بهترین انتخاب ممکن است فراخوانی 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
بین ۱ و ۱۰۰ است.
با این حال، این یک راهحل ایدهآل نیست: اگر بسیار حیاتی باشد که برنامه فقط بر روی مقادیر بین ۱ و ۱۰۰ عمل کند، و برنامه توابع زیادی با این نیاز داشته باشد، داشتن چنین بررسیهایی در هر تابع خستهکننده خواهد بود (و ممکن است عملکرد را تحت تأثیر قرار دهد).
در عوض، میتوانیم یک نوع جدید ایجاد کنیم و اعتبارسنجیها را در یک تابع برای ایجاد یک نمونه از نوع جدید قرار دهیم به جای تکرار اعتبارسنجیها در همهجا. به این ترتیب، استفاده از نوع جدید در امضاهای توابع ایمن است و میتوان با اطمینان از مقادیری که دریافت میکنند استفاده کرد. لیست ۹-۱۳ یک روش برای تعریف یک نوع Guess
را نشان میدهد که فقط یک نمونه از Guess
ایجاد میکند اگر تابع new
مقداری بین ۱ و ۱۰۰ دریافت کند.
#![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
که فقط با مقادیر بین ۱ و ۱۰۰ ادامه میدهدابتدا یک ساختار داده به نام 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
ساختار داده Guess
خصوصی است. مهم است که فیلد value
خصوصی باشد تا کدی که از ساختار Guess
استفاده میکند مجاز نباشد مقدار value
را مستقیماً تنظیم کند: کدی که خارج از ماژول است باید از تابع Guess::new
برای ایجاد یک نمونه از Guess
استفاده کند، و بنابراین تضمین میشود که هیچ راهی برای ایجاد یک Guess
با مقدار value
وجود ندارد که توسط شرایط در تابع Guess::new
بررسی نشده باشد.
تابعی که یک پارامتر میگیرد یا فقط اعدادی بین ۱ و ۱۰۰ بازمیگرداند میتواند در امضای خود اعلام کند که یک Guess
میگیرد یا بازمیگرداند به جای یک i32
و نیازی به انجام بررسیهای اضافی در بدنه خود ندارد.
خلاصه
ویژگیهای مدیریت خطای Rust طراحی شدهاند تا به شما کمک کنند کدی قدرتمندتر بنویسید. ماکروی panic!
نشان میدهد که برنامه شما در حالتی قرار دارد که نمیتواند آن را مدیریت کند و به شما امکان میدهد فرآیند را متوقف کنید به جای اینکه سعی کنید با مقادیر نامعتبر یا نادرست ادامه دهید. Enum Result
از سیستم نوع Rust استفاده میکند تا نشان دهد که عملیات ممکن است به روشی شکست بخورد که کد شما میتواند از آن بازیابی کند. میتوانید از Result
برای اطلاع دادن به کدی که کد شما را فراخوانی میکند استفاده کنید که باید موفقیت یا شکست احتمالی را نیز مدیریت کند. استفاده از panic!
و Result
در شرایط مناسب باعث میشود کد شما در برابر مشکلات اجتنابناپذیر قابل اطمینانتر شود.
حالا که راههای مفید استفاده کتابخانه استاندارد از جنریکها با Enums Option
و Result
را دیدهاید، درباره نحوه عملکرد جنریکها و نحوه استفاده از آنها در کد خود صحبت خواهیم کرد.