مقدمه

در درس قبل دیدیم که ماکروی panic! برای خطاهای غیرمنتظره و بحرانی که نشان‌دهنده یک باگ هستند، استفاده می‌شود. اما بیشتر خطاها در این دسته قرار نمی‌گیرند. بسیاری از خطاها، مانند عدم موفقیت در باز کردن یک فایل یا دریافت یک پاسخ نامعتبر از شبکه، قابل انتظار هستند و برنامه باید بتواند به صورت کنترل‌شده با آنها برخورد کند. برای این نوع خطاهای «قابل بازیابی»، Rust از یک enum بسیار قدرتمند به نام Result<T, E> استفاده می‌کند.

معرفی Result<T, E>

enum یا شمارشی Result به صورت زیر در کتابخانه استاندارد تعریف شده است:

Copy Icon RUST
enum Result<T, E> {
    Ok(T),
    Err(E),
}

این enum دو واریانت دارد: Ok(T) که در صورت موفقیت‌آمیز بودن عملیات، مقدار موفقیت از نوع T را در خود جای می‌دهد، و Err(E) که در صورت بروز خطا، اطلاعات خطا از نوع E را در بر می‌گیرد. با استفاده از این enum، یک تابع می‌تواند به صورت صریح اعلام کند که خروجی آن ممکن است با خطا همراه باشد. این کار به کامپایلر اجازه می‌دهد تا ما را مجبور به مدیریت هر دو حالت موفقیت و خطا کند و از نادیده گرفتن خطاهای احتمالی جلوگیری می‌کند.

بیایید این مفهوم را با تلاش برای باز کردن یک فایل ببینیم. تابع File::open() یک Result<std::fs::File, std::io::Error> برمی‌گرداند.

Copy Icon src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

در اینجا، نوع متغیر greeting_file_result یک Result است، نه خود فایل. برای دسترسی به فایل (در صورت موفقیت) یا مدیریت خطا، باید این Result را پردازش کنیم.

مدیریت Result با عبارت match

ابزار اصلی برای کار با enumها در Rust، عبارت match است. match به ما اجازه می‌دهد تا بر اساس واریانت یک enum، کدهای متفاوتی را اجرا کنیم. کامپایلر Rust تضمین می‌کند که ما تمام واریانت‌های ممکن را پوشش داده‌ایم.

Copy Icon src/main.rs
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

این مثال کمی پیچیده است، اما یک الگوی مدیریت خطای قوی را نشان می‌دهد. ابتدا سعی می‌کنیم فایل را باز کنیم. اگر نتیجه Ok باشد، خود فایل را برمی‌گردانیم. اگر Err باشد، با یک match تودرتو، نوع خطا را بررسی می‌کنیم. اگر نوع خطا NotFound باشد، سعی می‌کنیم فایل را ایجاد کنیم. اگر ایجاد فایل هم با خطا مواجه شود، برنامه را panic می‌کنیم. برای هر نوع خطای دیگری نیز برنامه panic می‌شود.

میانبرهایی برای مدیریت خطا

استفاده از match بسیار قدرتمند است، اما می‌تواند کد را کمی طولانی کند. کتابخانه استاندارد Rust متدهای کمکی زیادی را روی Result و Option ارائه می‌دهد که مدیریت خطا را ساده‌تر می‌کنند.

متدهای unwrap و expect

دو متد unwrap و expect به عنوان میانبر برای match عمل می‌کنند.

  • unwrap(): اگر Result یک Ok باشد، مقدار داخل آن را برمی‌گرداند. اگر یک Err باشد، برنامه را با پیام خطای پیش‌فرض panic می‌کند.
  • expect(message): مشابه unwrap عمل می‌کند، با این تفاوت که در صورت panic، پیام سفارشی که شما ارائه کرده‌اید را نمایش می‌دهد.
Copy Icon src/main.rs
// This will panic if hello.txt doesn't exist
let greeting_file = File::open("hello.txt").unwrap();

// This will panic with a custom message if hello.txt doesn't exist
let greeting_file = File::open("hello.txt")
    .expect("hello.txt should be included in this project");

هرچند این متدها کد را کوتاه‌تر می‌کنند، اما آنها panic می‌کنند و از بازیابی خطا جلوگیری می‌کنند. استفاده از آنها معمولاً در نمونه‌سازی سریع، تست‌ها، یا زمانی که منطق برنامه تضمین می‌کند که مقدار حتماً Ok خواهد بود، مناسب است.

در این درس با Result<T, E> به عنوان روش اصولی Rust برای مدیریت خطاهای قابل بازیابی آشنا شدیم. دیدیم که چگونه با استفاده از match می‌توانیم هر دو حالت موفقیت و خطا را به صورت ایمن مدیریت کنیم. این رویکرد یکی از ارکان اصلی نوشتن نرم‌افزار قوی و قابل اعتماد در Rust است. اما مدیریت تمام Resultها با match می‌تواند کمی تکراری باشد. در درس بعدی با اپراتور `?` به عنوان یک میانبر قدرتمند برای انتشار خطاها آشنا خواهیم شد.