Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

آیا باید از 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 تجزیه کنیم تا اجازه دهیم اعداد منفی نیز در نظر گرفته شوند، و سپس یک بررسی برای اینکه عدد در محدوده است یا نه اضافه کنیم، مانند زیر:

Filename: src/main.rs
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 دریافت کرده باشد.

Filename: src/guessing_game.rs
#![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
    }
}
}
Listing 9-13: نوع 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 را دیده‌اید، درباره نحوه عملکرد جنریک‌ها و نحوه استفاده از آن‌ها در کد خود صحبت خواهیم کرد.