معالجة الأخطاء في Rust: try/catch وما بعدها — المستوى المتقدم
\n\nعلى عكس العديد من اللغات حيث الاستثناءات هي الآلية القياسية (مثل try/catch في Python أو Java أو C++)، تقدم Rust نهجًا مختلفًا جوهريًا. لا توجد استثناءات بالمعنى الكلاسيكي هنا. بدلاً من ذلك، تستخدم اللغة نظام أنواع قوي لتمثيل الأخطاء ومعالجتها. قد يبدو هذا معقدًا للمبتدئين، ولكن على المستوى المتقدم ستقدر الأمان والقابلية للتنبؤ التي يوفرها هذا النهج. في هذه المقالة، سنشرح كيفية العمل مع الأخطاء في Rust بشكل احترافي: من الأساسيات Result و Option إلى الكريتات المتقدمة anyhow و thiserror، وسنقوم أيضًا بعمل مقارنات مع try/catch المألوف.
1. الأساس: Result وOption والعامل ?
\n\nأساس معالجة الأخطاء في Rust هو التعداد Result<T, E>. يمكن أن يكون إما Ok(T) (نجاح) أو Err(E) (خطأ). يمكن اعتبار العامل ? بمثابة النظير لـ try/catch. فهو يبسط تمرير الخطأ إلى الأعلى: إذا كانت القيمة Err، تقوم الدالة بإرجاعها فورًا؛ إذا كانت Ok، فإنها تستخرج القيمة.
use std::fs::File;\nuse std::io::Read;\n\nfn read_file(path: &str) -> Result<String, std::io::Error> {\n let mut file = File::open(path)?; // نظير try: تمرير الخطأ\n let mut content = String::new();\n file.read_to_string(&mut content)?;\n Ok(content)\n}\n\nفي هذا المثال، يمكن لكل من File::open و read_to_string إرجاع Err. العامل ? يحل محل match المرهق ويجعل الكود نظيفًا، مثل try/catch في اللغات الأخرى. ومع ذلك، على عكس الاستثناءات، فإنك تحدد نوع الخطأ صراحةً في توقيع الدالة - مما يعزز قابلية القراءة والأمان.
يُستخدم التعداد Option<T> للحالات التي قد تكون فيها القيمة مفقودة (نظير None في Python). يعمل العامل ? معه أيضًا:
fn get_first_element(vec: &Vec<i32>) -> Option<&i32> {\n vec.first()? // إذا كان المتجه فارغًا، نعيد None\n}\n\n2. العمل المتقدم مع أنواع الأخطاء: السمات From وError
\n\nعندما تستدعي دالة عدة عمليات بأنواع أخطاء مختلفة (مثل الإدخال/الإخراج والتحليل)، تظهر مشكلة: كيف نعيد نوعًا واحدًا؟ الحل هو السمة From ونوع خطأ مخصص.
لننشئ هيكل الخطأ الخاص بنا ونطبق عليه السمة std::error::Error (تلقائيًا عبر #[derive(Debug)] وتنفيذ Display يدويًا):
use std::fmt;\nuse std::io;\n\n#[derive(Debug)]\nenum MyError {\n Io(io::Error),\n Parse(std::num::ParseIntError),\n}\n\nimpl fmt::Display for MyError {\n fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n match self {\n MyError::Io(e) => write!(f, "IO error: {}", e),\n MyError::Parse(e) => write!(f, "Parse error: {}", e),\n }\n }\n}\n\nimpl std::error::Error for MyError {}\n\n// ننفذ From للتحويل التلقائي\nimpl From<io::Error> for MyError {\n fn from(error: io::Error) -> Self {\n MyError::Io(error)\n }\n}\n\nimpl From<std::num::ParseIntError> for MyError {\n fn from(error: std::num::ParseIntError) -> Self {\n MyError::Parse(error)\n }\n}\n\nfn process_data(path: &str) -> Result<i32, MyError> {\n let content = std::fs::read_to_string(path)?; // يتم التحويل تلقائيًا إلى MyError::Io\n let number: i32 = content.trim().parse()?; // يتم التحويل تلقائيًا إلى MyError::Parse\n Ok(number)\n}\n\nالآن، تقوم الدالة process_data بإرجاع نوع واحد MyError، ويقوم العامل ? بتحويل الأخطاء تلقائيًا بفضل السمة المطبقة