الملكية في Rust: النظام الذي يمنع أعطال الذاكرة نهائياً
القواعد الثلاث للملكية
نظام الملكية هو القلب النابض لـ Rust — السبب الذي يجعلها آمنة بدون جامع قمامة. القواعد بسيطة لكن آثارها عميقة:
- كل قيمة في Rust لها مالك واحد فقط (متغير واحد)
- لا يمكن أن يكون لقيمة أكثر من مالك في نفس الوقت
- عندما يخرج المالك من النطاق، تُحرّر القيمة تلقائياً
fn main() {
{
let readings = vec![23.5, 24.1, 22.8]; // readings تملك هذا Vec
println!("عدد القراءات: {}", readings.len());
} // readings خرجت من النطاق → الذاكرة تُحرّر تلقائياً
// لا يمكن استخدام readings هنا — لم تعد موجودة
}
في C++ كنت ستحتاج delete أو free يدوياً — أو تعتمد على جامع قمامة يوقف البرنامج أحياناً. Rust تفعل هذا تلقائياً بدون أي كلفة وقت التشغيل.
المكدس مقابل الكومة: أين تعيش البيانات؟
لفهم الملكية، تحتاج معرفة أين تُخزّن البيانات:
| المكدس (Stack) | الكومة (Heap) |
|---|---|
| سريع جداً | أبطأ (يحتاج تخصيص) |
| حجم ثابت معروف وقت التجميع | حجم متغير وقت التشغيل |
i32, f64, bool, char |
String, Vec<T>, HashMap |
| نسخ تلقائي (Copy) | نقل ملكية (Move) |
// على المكدس — نسخ بسيط
let a: i32 = 42;
let b = a; // b نسخة مستقلة — a لا تزال صالحة
println!("{} {}", a, b); // يعمل!
// على الكومة — نقل ملكية
let s1 = String::from("مستشعر_حرارة");
let s2 = s1; // الملكية انتقلت إلى s2 — s1 لم تعد صالحة
// println!("{}", s1); // خطأ تجميع! s1 نُقلت
println!("{}", s2); // يعمل
دلالات النقل: نقل الملكية
عندما تُسند قيمة على الكومة لمتغير آخر أو تمررها لدالة، تنتقل الملكية — المتغير الأصلي يصبح غير صالح:
fn process_data(data: Vec<f64>) {
println!("معالجة {} قراءة", data.len());
// data تُحرّر هنا عند خروجها من النطاق
}
fn main() {
let sensor_data = vec![23.5, 24.1, 22.8, 25.0];
process_data(sensor_data); // الملكية انتقلت إلى الدالة
// sensor_data لم تعد صالحة — المُجمِّع يمنع استخدامها
// println!("{:?}", sensor_data); // خطأ تجميع!
}
لماذا؟ لأن Rust تضمن أن مسؤول واحد فقط يحرر الذاكرة — لا تحرير مزدوج (double free)، لا مؤشرات معلّقة.
تدفق البيانات في خط إنتاج
fn read_sensors() -> Vec<f64> {
vec![23.5, 24.1, 22.8] // تنشئ وتُعيد الملكية للمستدعي
}
fn filter_alarms(data: Vec<f64>) -> Vec<f64> {
data.into_iter().filter(|&t| t > 24.0).collect()
}
fn log_alarms(alarms: Vec<f64>) {
for temp in &alarms {
println!("إنذار: {:.1}°C", temp);
}
}
fn main() {
let data = read_sensors(); // main تملك data
let alarms = filter_alarms(data); // الملكية انتقلت — data لم تعد صالحة
log_alarms(alarms); // الملكية انتقلت مرة أخرى
}
البيانات تتدفق من قارئ → مُعالج → مُسجّل، كل خطوة تملك البيانات مؤقتاً ثم تنقلها.
النسخ والاستنساخ: متى تُكرَّر القيم؟
Copy: نسخ تلقائي للأنواع البسيطة
الأنواع الصغيرة الموجودة على المكدس تُنسخ تلقائياً:
let sensor_id: u32 = 42;
let backup_id = sensor_id; // نسخة — كلاهما صالح
println!("أصلي: {}, نسخة: {}", sensor_id, backup_id);
الأنواع التي تدعم Copy: i32, f64, bool, char, (i32, f64) (tuples من أنواع Copy).
Clone: نسخ صريح للأنواع المعقدة
لنسخ قيمة على الكومة، استخدم .clone() — نسخ عميق صريح:
let original = String::from("خط إنتاج A");
let copy = original.clone(); // نسخ عميق — كلاهما صالح
println!("أصلي: {}", original);
println!("نسخة: {}", copy);
تنبيه: .clone() ينسخ كل البيانات — إذا كان لديك Vec بمليون عنصر، سيُنشئ نسخة كاملة. استخدمه بحذر في الأنظمة الصناعية حيث الأداء مهم.
الملكية في التطبيق: خط معالجة بيانات المستشعرات
لنجمع كل ما تعلمناه في مثال واقعي:
struct SensorReading {
id: u32, // Copy — على المكدس
timestamp: u64, // Copy
value: f64, // Copy
unit: String, // Move — على الكومة
}
fn create_reading(id: u32, value: f64, unit: &str) -> SensorReading {
SensorReading {
id,
timestamp: 1700000000,
value,
unit: String::from(unit),
}
}
fn format_reading(reading: SensorReading) -> String {
// reading انتقلت إلى هنا — المستدعي لن يستخدمها بعد
format!("[{}] المستشعر {}: {:.1} {}",
reading.timestamp, reading.id, reading.value, reading.unit)
}
fn main() {
let r = create_reading(101, 23.5, "°C");
let formatted = format_reading(r);
// r لم تعد صالحة — انتقلت إلى format_reading
println!("{}", formatted);
}
هذا التدفق واضح: كل دالة تملك بياناتها، وعند انتهائها تُحرّر تلقائياً. لا تسريبات، لا أخطاء.
الخلاصة
نظام الملكية يضمن أمان الذاكرة بثلاث قواعد بسيطة: مالك واحد، وقت واحد، تحرير تلقائي. الأنواع البسيطة (أعداد، منطقيات) تُنسخ تلقائياً عبر Copy. الأنواع المعقدة (String, Vec) تُنقل — ولنسخها تحتاج .clone(). هذا النظام يمنع تسريبات الذاكرة والمؤشرات المعلّقة بدون أي كلفة وقت التشغيل. لكن ماذا لو أردت مشاركة البيانات بدون نقلها؟ هذا موضوع الدرس القادم — الاستعارة والمراجع.