معالجة الأخطاء في Rust: بناء برامج صناعية لا تنهار أبداً
لماذا لا تملك Rust استثناءات
في معظم اللغات، الأخطاء تُرمى كاستثناءات يمكن أن تظهر في أي مكان. في Rust، الأخطاء هي قيم تُعاد من الدوال — تماماً كأي قيمة أخرى.
هذا يعني أن المُترجم يُجبرك على التعامل مع كل خطأ محتمل. لا يمكن لخطأ أن يمر دون معالجة كما يحدث مع الاستثناءات المنسية.
// في لغات أخرى: الخطأ قد يُرمى في أي سطر ويتم تجاهله
// sensor.read() // قد تُرمى استثناء ولا أحد يعالجه!
// في Rust: الخطأ جزء صريح من نوع القيمة المُعادة
// fn read_sensor() -> Result<f64, SensorError>
// لا يمكنك تجاهل النتيجة — المترجم سيحذرك
Option: قيم قد لا تكون موجودة
Option يُمثّل قيمة قد تكون موجودة (Some) أو غير موجودة (None).
هذا بديل آمن لـ null الذي يسبب أعطالاً في اللغات الأخرى.
struct SensorRegistry {
sensors: Vec<(u32, String)>,
}
impl SensorRegistry {
// البحث عن مستشعر — قد لا يكون موجوداً
fn find_sensor(&self, id: u32) -> Option<&String> {
self.sensors.iter()
.find(|(sensor_id, _)| *sensor_id == id)
.map(|(_, name)| name)
}
}
fn main() {
let registry = SensorRegistry {
sensors: vec![
(101, String::from("حرارة الفرن")),
(102, String::from("ضغط الخط")),
],
};
// معالجة القيمة الموجودة وغير الموجودة
match registry.find_sensor(101) {
Some(name) => println!("وُجد المستشعر: {}", name),
None => println!("المستشعر غير موجود"),
}
// أو باستخدام if let للإيجاز
if let Some(name) = registry.find_sensor(999) {
println!("المستشعر: {}", name);
} else {
println!("المستشعر 999 غير مسجّل في النظام");
}
}
Result<T, E>: عمليات قد تفشل
Result يُمثّل عملية قد تنجح (Ok) أو تفشل (Err).
هذا هو النوع الأساسي لمعالجة الأخطاء في Rust.
use std::num::ParseFloatError;
#[derive(Debug)]
enum SensorError {
Timeout,
Disconnected,
InvalidReading(String),
}
// قراءة سجل من وحدة تحكم صناعية
fn read_register(address: u16) -> Result<f64, SensorError> {
if address == 0 {
return Err(SensorError::Disconnected);
}
if address > 1000 {
return Err(SensorError::Timeout);
}
// محاكاة قراءة ناجحة
Ok(42.5)
}
fn main() {
match read_register(100) {
Ok(value) => println!("القراءة: {}", value),
Err(SensorError::Timeout) => println!("انتهت مهلة الاتصال"),
Err(SensorError::Disconnected) => println!("الجهاز غير متصل"),
Err(SensorError::InvalidReading(msg)) => {
println!("قراءة غير صالحة: {}", msg);
}
}
}
العامل ?: تمرير أنيق للأخطاء
العامل ? يُمرر الخطأ تلقائياً إلى الدالة المُستدعية.
بدلاً من كتابة match في كل سطر، سطر واحد يكفي.
#[derive(Debug)]
enum SystemError {
SensorFailed(String),
CalibrationError,
OutOfRange { value: f64, min: f64, max: f64 },
}
fn read_temperature() -> Result<f64, SystemError> {
// إذا فشلت، الخطأ يُعاد تلقائياً
Ok(85.5)
}
fn calibrate(raw: f64) -> Result<f64, SystemError> {
if raw < -50.0 || raw > 200.0 {
return Err(SystemError::OutOfRange {
value: raw, min: -50.0, max: 200.0,
});
}
Ok(raw * 1.02 + 0.5) // تطبيق معامل المعايرة
}
// العامل ? يجعل السلسلة نظيفة وقابلة للقراءة
fn get_calibrated_reading() -> Result<f64, SystemError> {
let raw = read_temperature()?; // فشل؟ يُعاد الخطأ
let calibrated = calibrate(raw)?; // فشل؟ يُعاد الخطأ
Ok(calibrated)
}
fn main() {
match get_calibrated_reading() {
Ok(temp) => println!("الحرارة المُعايرة: {:.1}°C", temp),
Err(e) => println!("خطأ: {:?}", e),
}
}
أنواع أخطاء مخصصة مع enum
أنشئ تعداداً يجمع كل أنواع الأخطاء الممكنة في نظامك.
use std::fmt;
#[derive(Debug)]
enum SensorError {
Timeout { address: u16, ms: u64 },
Disconnected,
InvalidReading(f64),
ProtocolError(String),
}
// تطبيق Display لرسائل خطأ واضحة
impl fmt::Display for SensorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SensorError::Timeout { address, ms } => {
write!(f, "انتهت المهلة للعنوان {} بعد {}ms", address, ms)
}
SensorError::Disconnected => {
write!(f, "المستشعر غير متصل")
}
SensorError::InvalidReading(val) => {
write!(f, "قراءة غير صالحة: {}", val)
}
SensorError::ProtocolError(msg) => {
write!(f, "خطأ في البروتوكول: {}", msg)
}
}
}
}
التحويل بين أنواع الأخطاء: سمة From
عند استخدام ?، يحتاج Rust أحياناً لتحويل نوع الخطأ.
سمة From تُعرّف كيفية التحويل التلقائي.
use std::io;
#[derive(Debug)]
enum MachineError {
IoError(String),
SensorError(String),
ParseError(String),
}
// التحويل من io::Error إلى MachineError
impl From<io::Error> for MachineError {
fn from(err: io::Error) -> Self {
MachineError::IoError(err.to_string())
}
}
// التحويل من ParseFloatError
impl From<std::num::ParseFloatError> for MachineError {
fn from(err: std::num::ParseFloatError) -> Self {
MachineError::ParseError(err.to_string())
}
}
// الآن يمكن استخدام ? مع أي من هذه الأخطاء
fn read_config_value(path: &str) -> Result<f64, MachineError> {
let content = std::fs::read_to_string(path)?; // io::Error -> MachineError
let value: f64 = content.trim().parse()?; // ParseFloatError -> MachineError
Ok(value)
}
متى تستخدم unwrap و expect ومتى لا
unwrap() و expect() يوقفان البرنامج عند الخطأ.
مفيدان في النماذج الأولية، خطيران في الإنتاج.
// في النماذج الأولية والاختبارات — مقبول
fn prototype_test() {
let value: f64 = "42.5".parse().unwrap();
let config = std::fs::read_to_string("config.toml").unwrap();
}
// expect أفضل: يُعطي رسالة واضحة عند الفشل
fn better_prototype() {
let value: f64 = "42.5".parse()
.expect("فشل تحليل قيمة المعايرة");
}
// في الإنتاج — استخدم Result والعامل ?
fn production_code() -> Result<f64, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("calibration.conf")?;
let value: f64 = content.trim().parse()?;
Ok(value)
}
// unwrap_or و unwrap_or_else: بدائل آمنة
fn safe_alternatives() {
// قيمة افتراضية عند الفشل
let timeout: u64 = "invalid".parse().unwrap_or(5000);
// حساب القيمة الافتراضية عند الحاجة فقط
let threshold: f64 = std::env::var("THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_else(|| {
println!("استخدام العتبة الافتراضية");
100.0
});
println!("المهلة: {}ms، العتبة: {}", timeout, threshold);
}
الخلاصة
- Rust تتعامل مع الأخطاء كـقيم وليس استثناءات — لا يمكن تجاهلها
- Option للقيم التي قد لا تكون موجودة (بديل آمن لـ null)
- Result للعمليات التي قد تفشل — يحمل إما النتيجة أو الخطأ
- العامل ? يُمرر الأخطاء بأناقة دون كتابة match متكررة
- أنواع الأخطاء المخصصة مع enum تصف كل حالات الفشل الممكنة
- سمة From تُمكّن التحويل التلقائي بين أنواع الأخطاء
- unwrap للنماذج الأولية فقط — في الإنتاج استخدم Result و ?