خطاهای قابل بازیابی با Result
بیشتر خطاها به اندازهای جدی نیستند که نیاز به توقف کامل برنامه داشته باشند. گاهی اوقات وقتی یک تابع با شکست مواجه میشود، دلیلی وجود دارد که میتوانید آن را به راحتی تفسیر کرده و به آن پاسخ دهید. برای مثال، اگر بخواهید یک فایل را باز کنید و این عملیات به دلیل وجود نداشتن فایل شکست بخورد، ممکن است بخواهید فایل را ایجاد کنید به جای اینکه فرآیند را متوقف کنید.
به یاد بیاورید از بخش “Handling Potential Failure with Result
”
در فصل ۲ که Result
به صورت یک enum تعریف شده که دو حالت دارد، Ok
و Err
، به صورت زیر:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
و E
پارامترهای نوع جنریک هستند: ما درباره جنریکها به طور کاملتر در فصل ۱۰ صحبت خواهیم کرد.
چیزی که اکنون باید بدانید این است که T
نمایانگر نوع مقداری است که در حالت موفقیت در داخل Ok
بازگردانده میشود، و E
نمایانگر نوع خطایی است که در حالت شکست در داخل Err
بازگردانده میشود.
زیرا Result
این پارامترهای نوع جنریک را دارد، میتوانیم نوع Result
و توابع تعریف شده روی آن را
در بسیاری از شرایط مختلف که مقادیر موفقیت و خطا ممکن است متفاوت باشند، استفاده کنیم.
بیایید تابعی را فراخوانی کنیم که یک مقدار Result
را بازمیگرداند زیرا این تابع ممکن است با شکست
مواجه شود. در لیست ۹-۳ سعی میکنیم یک فایل را باز کنیم.
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
نوع بازگشتی File::open
یک Result<T, E>
است. پارامتر نوع جنریک T
توسط پیادهسازی
File::open
با نوع مقدار موفقیت، یعنی std::fs::File
، که یک فایل هندل است، مقداردهی
شده است. نوع E
استفاده شده در مقدار خطا std::io::Error
است. این نوع بازگشتی به این معنی
است که فراخوانی File::open
ممکن است موفقیتآمیز باشد و یک فایل هندل بازگرداند که میتوانیم از
آن برای خواندن یا نوشتن استفاده کنیم. همچنین ممکن است این فراخوانی با شکست مواجه شود: برای مثال،
فایل ممکن است وجود نداشته باشد یا ممکن است مجوز دسترسی به فایل را نداشته باشیم. تابع File::open
باید روشی داشته باشد تا به ما بگوید که آیا موفقیتآمیز بود یا شکست خورد و در عین حال فایل هندل یا
اطلاعات خطا را به ما بدهد. این اطلاعات دقیقاً همان چیزی است که enum Result
منتقل میکند.
در حالتی که File::open
موفقیتآمیز باشد، مقدار در متغیر greeting_file_result
یک نمونه از Ok
خواهد بود که یک فایل هندل را شامل میشود. در حالتی که با شکست مواجه شود، مقدار در
greeting_file_result
یک نمونه از Err
خواهد بود که اطلاعات بیشتری در مورد نوع خطایی که رخ
داده است را شامل میشود.
باید به کد در لیست ۹-۳ اضافه کنیم تا اقدامات متفاوتی بسته به مقداری که File::open
بازمیگرداند
انجام دهیم. لیست ۹-۴ یک روش برای مدیریت Result
با استفاده از یک ابزار پایه، یعنی عبارت match
که در فصل ۶ مورد بحث قرار گرفت، نشان میدهد.
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {error:?}"), }; }
match
برای مدیریت حالتهای Result
که ممکن است بازگردانده شودتوجه داشته باشید که مانند enum Option
، enum Result
و حالات آن به وسیله prelude به محدوده آورده شدهاند، بنابراین نیازی نیست قبل از حالات Ok
و Err
در بازوهای match
از Result::
استفاده کنیم.
وقتی نتیجه Ok
باشد، این کد مقدار داخلی file
را از حالت Ok
بازمیگرداند و سپس آن مقدار فایل هندل را به متغیر greeting_file
اختصاص میدهیم. بعد از match
، میتوانیم از فایل هندل برای خواندن یا نوشتن استفاده کنیم.
بازوی دیگر match
حالت زمانی را مدیریت میکند که از File::open
یک مقدار Err
دریافت میکنیم. در این مثال، تصمیم گرفتهایم ماکروی panic!
را فراخوانی کنیم. اگر فایل hello.txt در دایرکتوری فعلی ما وجود نداشته باشد و این کد را اجرا کنیم، خروجی زیر را از ماکروی panic!
خواهیم دید:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
مثل همیشه، این خروجی دقیقاً به ما میگوید چه اشتباهی رخ داده است.
مطابقت بر اساس خطاهای مختلف
کد در لیست ۹-۴ در هر صورتی که File::open
با شکست مواجه شود، ماکروی panic!
را فراخوانی میکند. با این حال، ما میخواهیم اقدامات متفاوتی برای دلایل مختلف شکست انجام دهیم. اگر File::open
به دلیل وجود نداشتن فایل شکست بخورد، میخواهیم فایل را ایجاد کنیم و هندل فایل جدید را بازگردانیم. اگر File::open
به دلایل دیگری شکست بخورد—برای مثال، به دلیل نداشتن مجوز باز کردن فایل—همچنان میخواهیم کد مانند لیست ۹-۴ panic!
کند. برای این کار، یک عبارت match
داخلی اضافه میکنیم که در لیست ۹-۵ نشان داده شده است.
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:?}");
}
},
};
}
نوع مقداری که File::open
درون حالت Err
بازمیگرداند، io::Error
است که یک ساختار داده ارائه شده توسط کتابخانه استاندارد است. این ساختار دارای متدی به نام kind
است که میتوانیم آن را برای دریافت مقدار io::ErrorKind
فراخوانی کنیم. enum io::ErrorKind
توسط کتابخانه استاندارد ارائه شده و شامل حالتهایی است که انواع مختلف خطاهای ممکن در یک عملیات io
را نمایش میدهد. حالتی که میخواهیم از آن استفاده کنیم ErrorKind::NotFound
است که نشان میدهد فایل مورد نظر برای باز کردن هنوز وجود ندارد. بنابراین، ما بر روی greeting_file_result
مطابقت میدهیم، اما همچنین یک match
داخلی بر روی error.kind()
داریم.
شرطی که میخواهیم در match
داخلی بررسی کنیم این است که آیا مقدار بازگردانده شده توسط error.kind()
همان حالت NotFound
از enum ErrorKind
است یا خیر. اگر چنین باشد، سعی میکنیم فایل را با File::create
ایجاد کنیم. با این حال، از آنجایی که File::create
نیز ممکن است شکست بخورد، به یک بازوی دوم در عبارت match
داخلی نیاز داریم. هنگامی که فایل نمیتواند ایجاد شود، یک پیام خطای متفاوت چاپ میشود. بازوی دوم match
بیرونی به همان شکل باقی میماند، بنابراین برنامه برای هر خطایی به جز خطای وجود نداشتن فایل، با خطا متوقف میشود.
جایگزینهایی برای استفاده از match
با Result<T, E>
استفاده از match
زیاد است! عبارت match
بسیار مفید است اما همچنان ابتدایی محسوب میشود.
در فصل ۱۳، درباره closures یاد خواهید گرفت که در بسیاری از متدهایی که روی Result<T, E>
تعریف شدهاند استفاده میشوند. این متدها میتوانند هنگام مدیریت مقادیر Result<T, E>
در کد شما،
مختصرتر از استفاده از match
باشند.
برای مثال، در اینجا راه دیگری برای نوشتن همان منطق نشان داده شده در لیست ۹-۵ آورده شده است،
این بار با استفاده از closures و متد unwrap_or_else
:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
اگرچه این کد همان رفتار لیست ۹-۵ را دارد، اما شامل هیچ عبارت match
نیست و خواندن آن تمیزتر است.
بعد از خواندن فصل ۱۳، به این مثال بازگردید و متد unwrap_or_else
را در مستندات کتابخانه استاندارد
بررسی کنید. بسیاری از این متدها میتوانند عبارتهای match
تو در تو را هنگام کار با خطاها ساده کنند.
میانبرهایی برای توقف برنامه در صورت خطا: unwrap
و expect
استفاده از match
به اندازه کافی خوب کار میکند، اما ممکن است کمی طولانی باشد و همیشه به خوبی نیت
را منتقل نکند. نوع Result<T, E>
دارای بسیاری از متدهای کمکی است که برای انجام وظایف خاصتر تعریف
شدهاند. متد unwrap
یک روش میانبر است که دقیقاً مانند عبارت match
که در لیست ۹-۴ نوشتیم،
پیادهسازی شده است. اگر مقدار Result
در حالت Ok
باشد، unwrap
مقدار داخل Ok
را بازمیگرداند.
اگر مقدار Result
در حالت Err
باشد، unwrap
ماکروی panic!
را برای ما فراخوانی میکند. در اینجا
یک مثال از استفاده از unwrap
آورده شده است:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
اگر این کد را بدون فایل hello.txt اجرا کنیم، یک پیام خطا از فراخوانی panic!
که متد unwrap
انجام
میدهد خواهیم دید:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
به همین ترتیب، متد expect
به ما اجازه میدهد پیام خطای ماکروی panic!
را نیز انتخاب کنیم. استفاده
از expect
به جای unwrap
و ارائه پیامهای خطای خوب میتواند نیت شما را بهتر منتقل کند و پیگیری منبع
یک خطا را آسانتر کند. سینتکس expect
به این شکل است:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
ما از expect
به همان شیوهای استفاده میکنیم که از unwrap
استفاده میکنیم: برای بازگرداندن فایل هندل یا فراخوانی ماکروی panic!
. پیام خطایی که توسط expect
در فراخوانی panic!
استفاده میشود، پارامتری است که ما به expect
میدهیم، به جای پیام پیشفرض panic!
که توسط unwrap
استفاده میشود. اینجا چیزی است که به نظر میرسد:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
در کد با کیفیت تولید، بیشتر Rustaceanها expect
را به جای unwrap
انتخاب میکنند و اطلاعات بیشتری درباره اینکه چرا عملیات باید همیشه موفقیتآمیز باشد ارائه میدهند. به این ترتیب، اگر فرضیات شما هرگز اشتباه ثابت شوند، اطلاعات بیشتری برای استفاده در اشکالزدایی خواهید داشت.
انتشار خطاها (Propagating Errors)
وقتی پیادهسازی یک تابع چیزی را فراخوانی میکند که ممکن است شکست بخورد، به جای مدیریت خطا درون خود تابع، میتوانید خطا را به کدی که تابع را فراخوانی کرده است بازگردانید تا تصمیم بگیرد چه کاری انجام دهد. این به عنوان انتشار خطا شناخته میشود و کنترل بیشتری به کدی که فراخوانی میکند میدهد، جایی که ممکن است اطلاعات یا منطقی وجود داشته باشد که تعیین کند چگونه باید خطا مدیریت شود بیشتر از آنچه در زمینه کد شما موجود است.
برای مثال، لیست ۹-۶ یک تابع را نشان میدهد که یک نام کاربری را از یک فایل میخواند. اگر فایل وجود نداشته باشد یا قابل خواندن نباشد، این تابع آن خطاها را به کدی که تابع را فراخوانی کرده بازمیگرداند.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
match
این تابع میتواند به روشی بسیار کوتاهتر نوشته شود، اما ما قرار است با انجام بسیاری از کارها به صورت دستی، مدیریت خطاها را بررسی کنیم. در انتها، راه کوتاهتر را نشان خواهیم داد. بیایید ابتدا به نوع بازگشتی تابع نگاه کنیم: Result<String, io::Error>
. این به این معناست که تابع مقداری از نوع Result<T, E>
بازمیگرداند، جایی که پارامتر جنریک T
با نوع مشخص String
مقداردهی شده است و نوع جنریک E
با نوع مشخص io::Error
.
اگر این تابع بدون هیچ مشکلی موفقیتآمیز باشد، کدی که این تابع را فراخوانی میکند یک مقدار Ok
دریافت میکند که یک String
را نگهداری میکند—نام کاربریای که این تابع از فایل خوانده است. اگر این تابع با مشکلی مواجه شود، کدی که آن را فراخوانی کرده است یک مقدار Err
دریافت میکند که یک نمونه از io::Error
را نگهداری میکند که اطلاعات بیشتری درباره مشکلاتی که رخ دادهاند شامل میشود. ما io::Error
را به عنوان نوع بازگشتی این تابع انتخاب کردیم زیرا این همان نوعی است که مقدار خطا از هر دو عملیات فراخوانی شده در بدنه این تابع که ممکن است شکست بخورند بازمیگرداند: تابع File::open
و متد read_to_string
.
بدنه تابع با فراخوانی تابع File::open
شروع میشود. سپس مقدار Result
را با یک match
مشابه
آنچه در لیست ۹-۴ دیدیم مدیریت میکنیم. اگر File::open
موفق شود، هندل فایل در متغیر الگو file
به مقدار در متغیر قابل تغییر username_file
تبدیل میشود و تابع ادامه مییابد. در حالت Err
،
به جای فراخوانی panic!
، از کلیدواژه return
استفاده میکنیم تا زودتر از تابع خارج شویم و مقدار
خطا از File::open
که اکنون در متغیر الگو e
قرار دارد را به کدی که تابع را فراخوانی کرده بازگردانیم.
بنابراین، اگر یک هندل فایل در username_file
داشته باشیم، تابع سپس یک String
جدید در متغیر
username
ایجاد کرده و متد read_to_string
را روی هندل فایل در username_file
فراخوانی میکند
تا محتوای فایل را در username
بخواند. متد read_to_string
نیز یک مقدار Result
بازمیگرداند
زیرا ممکن است با شکست مواجه شود، حتی اگر File::open
موفق بوده باشد. بنابراین، به یک match
دیگر برای مدیریت آن Result
نیاز داریم: اگر read_to_string
موفق شود، آنگاه تابع ما موفقیتآمیز
بوده و نام کاربری از فایل که اکنون در username
است، درون یک Ok
بازمیگرداند. اگر
read_to_string
شکست بخورد، مقدار خطا را به همان شیوهای که مقدار خطا را در match
که مقدار
بازگشتی File::open
را مدیریت میکرد بازمیگردانیم. با این حال، نیازی نیست که به صراحت بگوییم
return
، زیرا این آخرین عبارت در تابع است.
کدی که این تابع را فراخوانی میکند سپس مدیریت دریافت مقدار Ok
که شامل یک نام کاربری است یا
مقدار Err
که شامل یک io::Error
است را انجام میدهد. این به کدی که تابع را فراخوانی میکند بستگی دارد
که تصمیم بگیرد با این مقادیر چه کاری انجام دهد. اگر کد فراخوانیکننده یک مقدار Err
دریافت کند،
میتواند panic!
را فراخوانی کرده و برنامه را متوقف کند، از یک نام کاربری پیشفرض استفاده کند، یا
به جای فایل نام کاربری را از مکان دیگری جستجو کند، برای مثال. ما اطلاعات کافی درباره اینکه کد فراخوانیکننده
دقیقاً چه میخواهد انجام دهد نداریم، بنابراین تمام اطلاعات موفقیت یا خطا را به بالا منتقل میکنیم
تا آن را به درستی مدیریت کند.
این الگوی انتشار خطاها در Rust آنقدر رایج است که Rust عملگر ?
را برای آسانتر کردن این کار
فراهم میکند.
یک میانبر برای انتشار خطاها: عملگر ?
لیست ۹-۷ پیادهسازی read_username_from_file
را نشان میدهد که همان عملکرد لیست ۹-۶ را دارد،
اما این پیادهسازی از عملگر ?
استفاده میکند.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
?
بازمیگرداندعملگر ?
که پس از یک مقدار Result
قرار میگیرد تقریباً به همان شیوهای عمل میکند که عبارات
match
که برای مدیریت مقادیر Result
در لیست ۹-۶ تعریف کردیم. اگر مقدار Result
در حالت
Ok
باشد، مقدار درون Ok
از این عبارت بازگردانده میشود و برنامه ادامه مییابد. اگر مقدار در حالت
Err
باشد، مقدار Err
از کل تابع بازگردانده میشود به گونهای که انگار کلیدواژه return
را
استفاده کردهایم تا مقدار خطا به کد فراخوانیکننده منتقل شود.
تفاوتی بین کاری که عبارت match
در لیست ۹-۶ انجام میدهد و کاری که عملگر ?
انجام میدهد وجود
دارد: مقادیر خطا که عملگر ?
روی آنها فراخوانی میشود از طریق تابع from
که در ویژگی
From
کتابخانه استاندارد تعریف شده است عبور میکنند، که برای تبدیل مقادیر از یک نوع به نوع دیگر
استفاده میشود. وقتی عملگر ?
تابع from
را فراخوانی میکند، نوع خطای دریافت شده به نوع خطای
تعریف شده در نوع بازگشتی تابع فعلی تبدیل میشود. این موضوع زمانی مفید است که یک تابع یک نوع خطا
را برای نمایش تمام راههایی که ممکن است یک تابع شکست بخورد بازگرداند، حتی اگر بخشهایی ممکن است
به دلایل بسیار مختلفی شکست بخورند.
برای مثال، میتوانیم تابع read_username_from_file
در لیست ۹-۷ را تغییر دهیم تا یک نوع خطای سفارشی به نام OurError
که تعریف کردهایم بازگرداند. اگر همچنین impl From<io::Error> for OurError
را تعریف کنیم تا یک نمونه از OurError
را از یک io::Error
بسازد، سپس فراخوانیهای عملگر ?
در بدنه تابع read_username_from_file
تابع from
را فراخوانی کرده و نوع خطاها را بدون نیاز به افزودن کد اضافی به تابع تبدیل میکنند.
در زمینه لیست ۹-۷، عملگر ?
در انتهای فراخوانی File::open
مقدار درون یک Ok
را به متغیر username_file
بازمیگرداند. اگر خطایی رخ دهد، عملگر ?
زودتر از کل تابع خارج شده و هر مقدار Err
را به کد فراخوانیکننده بازمیگرداند. همین موضوع برای عملگر ?
در انتهای فراخوانی read_to_string
صدق میکند.
عملگر ?
مقدار زیادی از کد اضافی را حذف کرده و پیادهسازی این تابع را سادهتر میکند. حتی میتوانیم این کد را بیشتر کوتاه کنیم با زنجیره کردن فراخوانی متدها بلافاصله بعد از ?
، همانطور که در لیست ۹-۸ نشان داده شده است.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } }
?
ما ایجاد String
جدید در username
را به ابتدای تابع منتقل کردهایم؛ آن قسمت تغییر نکرده است. به جای ایجاد یک متغیر username_file
، ما فراخوانی read_to_string
را مستقیماً به نتیجه File::open("hello.txt")?
زنجیره کردهایم. همچنان یک عملگر ?
در انتهای فراخوانی read_to_string
داریم و همچنان مقدار Ok
شامل username
را زمانی که هر دو File::open
و read_to_string
موفق هستند بازمیگردانیم، به جای بازگرداندن خطاها. عملکرد دوباره همانند لیست ۹-۶ و لیست ۹-۷ است؛ این فقط یک روش متفاوت و کاربرپسندتر برای نوشتن آن است.
لیست ۹-۹ روشی برای کوتاهتر کردن این کد با استفاده از fs::read_to_string
را نشان میدهد.
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
fs::read_to_string
به جای باز کردن و سپس خواندن فایلخواندن یک فایل به یک رشته یک عملیات نسبتاً رایج است، بنابراین کتابخانه استاندارد تابع مناسب fs::read_to_string
را فراهم میکند که فایل را باز میکند، یک String
جدید ایجاد میکند، محتوای فایل را میخواند، محتوا را در آن String
قرار میدهد و آن را بازمیگرداند. البته، استفاده از fs::read_to_string
به ما فرصتی برای توضیح تمام مدیریت خطاها نمیدهد، بنابراین ابتدا آن را به روش طولانیتر انجام دادیم.
جایی که میتوان از عملگر ?
استفاده کرد
عملگر ?
فقط در توابعی استفاده میشود که نوع بازگشتی آنها با مقدار استفاده شده توسط ?
سازگار باشد. این به این دلیل است که عملگر ?
برای بازگرداندن زودهنگام یک مقدار از تابع تعریف شده است، به همان شیوهای که عبارت match
در لیست ۹-۶ تعریف شده است. در لیست ۹-۶، match
از یک مقدار Result
استفاده میکرد و بازوی بازگشتی زودهنگام یک مقدار Err(e)
را بازمیگرداند. نوع بازگشتی تابع باید یک Result
باشد تا با این بازگشت سازگار باشد.
در لیست ۹-۱۰، بیایید به خطایی که دریافت میکنیم وقتی که از عملگر ?
در یک تابع main
با نوع بازگشتیای که با نوع مقدار استفاده شده در ?
سازگار نیست استفاده میکنیم نگاه کنیم.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
?
در تابع main
که نوع بازگشتی آن ()
است و کامپایل نمیشود.این کد یک فایل را باز میکند، که ممکن است شکست بخورد. عملگر ?
مقدار Result
بازگردانده شده توسط File::open
را دنبال میکند، اما این تابع main
نوع بازگشتی ()
دارد، نه Result
. وقتی این کد را کامپایل میکنیم، پیام خطای زیر را دریافت میکنیم:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
این خطا نشان میدهد که فقط میتوان از عملگر ?
در توابعی که نوع بازگشتی آنها Result
، Option
، یا نوع دیگری که FromResidual
را پیادهسازی میکند استفاده کرد.
برای رفع این خطا، دو انتخاب دارید. یکی این است که نوع بازگشتی تابع خود را تغییر دهید تا با مقداری که از عملگر ?
استفاده میکنید سازگار باشد، به شرطی که محدودیتی مانع از انجام این کار نداشته باشید. انتخاب دیگر این است که از یک match
یا یکی از متدهای Result<T, E>
برای مدیریت Result<T, E>
به شیوهای که مناسب است استفاده کنید.
پیام خطا همچنین اشاره کرد که ?
میتواند با مقادیر Option<T>
نیز استفاده شود. همانند استفاده از ?
روی Result
، فقط میتوانید از ?
روی Option
در تابعی استفاده کنید که یک Option
بازمیگرداند. رفتار عملگر ?
وقتی روی یک Option<T>
فراخوانی میشود شبیه به رفتار آن وقتی روی یک Result<T, E>
فراخوانی میشود: اگر مقدار None
باشد، None
زودهنگام از تابع بازگردانده میشود. اگر مقدار Some
باشد، مقدار داخل Some
مقدار نتیجه عبارت است و تابع ادامه میدهد. لیست ۹-۱۱ مثالی از تابعی را نشان میدهد که آخرین کاراکتر خط اول متن داده شده را پیدا میکند.
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } fn main() { assert_eq!( last_char_of_first_line("Hello, world\nHow are you today?"), Some('d') ); assert_eq!(last_char_of_first_line(""), None); assert_eq!(last_char_of_first_line("\nhi"), None); }
?
روی یک مقدار Option<T>
این تابع Option<char>
بازمیگرداند زیرا ممکن است یک کاراکتر وجود داشته باشد، اما ممکن است وجود نداشته باشد. این کد آرگومان قطعه رشته text
را میگیرد و متد lines
را روی آن فراخوانی میکند، که یک iterator روی خطوط درون رشته بازمیگرداند. چون این تابع میخواهد خط اول را بررسی کند، next
را روی iterator فراخوانی میکند تا اولین مقدار از iterator را دریافت کند. اگر text
رشتهای خالی باشد، این فراخوانی به next
مقدار None
بازمیگرداند، که در این صورت از ?
برای متوقف کردن و بازگرداندن None
از last_char_of_first_line
استفاده میکنیم. اگر text
رشته خالی نباشد، next
یک مقدار Some
شامل یک قطعه رشته از خط اول در text
بازمیگرداند.
عملگر ?
قطعه رشته را استخراج میکند و میتوانیم متد chars
را روی آن فراخوانی کنیم تا یک iterator از کاراکترهای آن دریافت کنیم. ما به آخرین کاراکتر در این خط اول علاقهمند هستیم، بنابراین متد last
را فراخوانی میکنیم تا آخرین مورد در iterator را بازگرداند. این یک Option
است زیرا ممکن است خط اول رشتهای خالی باشد؛ برای مثال، اگر text
با یک خط خالی شروع شود اما کاراکترهایی در خطوط دیگر داشته باشد، مانند "\nhi"
. با این حال، اگر آخرین کاراکتری در خط اول وجود داشته باشد، در حالت Some
بازگردانده میشود. عملگر ?
در میانه به ما راهی مختصر برای بیان این منطق میدهد و اجازه میدهد تابع را در یک خط پیادهسازی کنیم. اگر نمیتوانستیم از عملگر ?
روی Option
استفاده کنیم، باید این منطق را با فراخوانی متدهای بیشتر یا یک عبارت match
پیادهسازی میکردیم.
توجه داشته باشید که میتوانید از عملگر ?
روی یک Result
در یک تابع که یک Result
بازمیگرداند استفاده کنید، و میتوانید از عملگر ?
روی یک Option
در یک تابع که یک Option
بازمیگرداند استفاده کنید، اما نمیتوانید این دو را با هم ترکیب کنید. عملگر ?
به طور خودکار یک Result
را به یک Option
یا برعکس تبدیل نمیکند؛ در این موارد، میتوانید از متدهایی مانند ok
روی Result
یا ok_or
روی Option
برای تبدیل صریح استفاده کنید.
تا کنون، تمام توابع main
که استفاده کردهایم مقدار ()
بازمیگرداندند. تابع main
خاص است زیرا نقطه ورود و خروج یک برنامه اجرایی است، و محدودیتهایی در نوع بازگشتی آن وجود دارد تا برنامه همانطور که انتظار میرود رفتار کند.
خوشبختانه، main
میتواند یک Result<(), E>
نیز بازگرداند. لیست ۹-۱۲ کد لیست ۹-۱۰ را دارد، اما نوع بازگشتی main
را به Result<(), Box<dyn Error>>
تغییر دادهایم و یک مقدار بازگشتی Ok(())
به انتهای آن اضافه کردهایم. این کد اکنون کامپایل میشود.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
main
برای بازگرداندن Result<(), E>
اجازه میدهد از عملگر ?
روی مقادیر Result
استفاده شود.نوع Box<dyn Error>
یک شیء ویژگی (trait object) است که در بخش “Using Trait Objects that Allow for Values of Different Types” در فصل ۱۸ درباره آن صحبت خواهیم کرد. در حال حاضر، میتوانید Box<dyn Error>
را به معنای “هر نوع خطا” در نظر بگیرید. استفاده از ?
روی یک مقدار Result
در یک تابع main
با نوع خطای Box<dyn Error>
مجاز است زیرا این امکان را میدهد که هر مقدار Err
زودتر بازگردانده شود. اگرچه بدنه این تابع main
فقط خطاهای نوع std::io::Error
را بازمیگرداند، با مشخص کردن Box<dyn Error>
، این امضا حتی اگر کد بیشتری که خطاهای دیگری بازمیگرداند به بدنه main
اضافه شود، صحیح باقی میماند.
وقتی یک تابع main
یک Result<(), E>
بازمیگرداند، برنامه اجرایی با مقدار 0
خارج میشود اگر main
مقدار Ok(())
بازگرداند و با یک مقدار غیر صفر خارج میشود اگر main
مقدار Err
بازگرداند. برنامههای اجرایی نوشته شده در C هنگام خروج مقادیر صحیح بازمیگردانند: برنامههایی که با موفقیت خارج میشوند مقدار صحیح 0
را بازمیگردانند و برنامههایی که دچار خطا میشوند مقداری غیر از 0
بازمیگردانند. Rust نیز مقادیر صحیح را از برنامههای اجرایی بازمیگرداند تا با این قرارداد سازگار باشد.
تابع main
میتواند هر نوعی را که ویژگی std::process::Termination
را پیادهسازی میکند بازگرداند، که شامل تابع report
است که یک ExitCode
بازمیگرداند. مستندات کتابخانه استاندارد را برای اطلاعات بیشتر درباره پیادهسازی ویژگی Termination
برای انواع خودتان مطالعه کنید.
اکنون که جزئیات فراخوانی panic!
یا بازگرداندن Result
را بررسی کردیم، بیایید به موضوع نحوه تصمیمگیری درباره اینکه کدامیک در چه مواردی مناسب است بازگردیم.