الاستعارة والمراجع في Rust: مشاركة البيانات بأمان مطلق
المراجع الثابتة: القراءة بدون امتلاك
في الدرس السابق، رأينا أن تمرير قيمة لدالة ينقل الملكية. لكن ماذا لو أردت أن تقرأ دالة بيانات المستشعر بدون أخذ ملكيتها؟ هنا تأتي الاستعارة (Borrowing) عبر المراجع:
fn print_average(readings: &Vec<f64>) {
let sum: f64 = readings.iter().sum();
let avg = sum / readings.len() as f64;
println!("المتوسط: {:.2}°C", avg);
}
fn main() {
let data = vec![23.5, 24.1, 22.8, 25.0];
print_average(&data); // استعارة — data لا تزال ملكنا
print_average(&data); // يمكن استعارتها مرات عديدة
println!("عدد القراءات: {}", data.len()); // لا تزال صالحة!
}
&data تُنشئ مرجعاً ثابتاً — يسمح بالقراءة فقط. الدالة تستعير البيانات مؤقتاً وتُعيدها عند الانتهاء. يمكن إنشاء عدة مراجع ثابتة في نفس الوقت — لأن القراءة المتزامنة آمنة.
المراجع المتغيرة: الكتابة الحصرية
أحياناً تحتاج دالة تعديل البيانات. لهذا تستخدم &mut — مرجع متغير:
fn add_reading(readings: &mut Vec<f64>, value: f64) {
readings.push(value);
}
fn main() {
let mut data = vec![23.5, 24.1];
add_reading(&mut data, 22.8);
add_reading(&mut data, 25.0);
println!("القراءات: {:?}", data); // [23.5, 24.1, 22.8, 25.0]
}
القاعدة الحاسمة: لا يمكن أن يوجد أكثر من مرجع متغير واحد في نفس الوقت:
let mut data = vec![1.0, 2.0, 3.0];
let r1 = &mut data;
// let r2 = &mut data; // خطأ تجميع! مرجع متغير ثانٍ ممنوع
r1.push(4.0);
قواعد الاستعارة
المُجمِّع يفرض قاعدتين صارمتين:
القاعدة 1: مراجع ثابتة متعددة أو مرجع متغير واحد
let mut data = vec![1.0, 2.0, 3.0];
// ✅ مسموح: عدة مراجع ثابتة
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2);
// ✅ مسموح: مرجع متغير واحد (بعد انتهاء المراجع الثابتة)
let r3 = &mut data;
r3.push(4.0);
let mut data = vec![1.0, 2.0, 3.0];
// ❌ ممنوع: مرجع ثابت ومتغير في نفس الوقت
let r1 = &data;
let r2 = &mut data; // خطأ! لا يمكن الاستعارة المتغيرة أثناء وجود استعارة ثابتة
println!("{:?}", r1);
القاعدة 2: المراجع يجب أن تكون صالحة دائماً
المُجمِّع يضمن أن المرجع لن يعيش أطول من البيانات التي يشير إليها:
// ❌ هذا لن يعمل:
fn dangling_reference() -> &String {
let s = String::from("بيانات");
&s // خطأ! s تُحرّر عند نهاية الدالة — المرجع سيكون معلّقاً
}
// ✅ الحل: أعد الملكية
fn owned_value() -> String {
let s = String::from("بيانات");
s // نقل ملكية — آمن
}
لماذا هذه القواعد مهمة صناعياً؟
تخيّل خيطين (threads) يقرأان بيانات مستشعر:
- بدون قواعد Rust (C++): خيط يقرأ بينما آخر يُعدّل → سباق بيانات → قيم خاطئة → قرار تحكم خاطئ
- مع قواعد Rust: المُجمِّع يرفض الكود قبل التشغيل — المشكلة مستحيلة الحدوث
المراجع المعلّقة: ما تمنعه Rust
المرجع المعلّق (Dangling Reference) يشير إلى ذاكرة حُرّرت — سبب شائع للأعطال في C/C++. Rust تمنعها كلياً:
fn main() {
let reference;
{
let value = String::from("مؤقت");
reference = &value;
} // value حُرّرت هنا
// println!("{}", reference); // خطأ تجميع! reference تشير لذاكرة محررة
}
المُجمِّع يتتبع عمر (lifetime) كل مرجع ويضمن أنه لن يشير أبداً لذاكرة غير صالحة.
مقدمة في فترات الحياة
أحياناً يحتاج المُجمِّع مساعدتك لفهم أعمار المراجع. هنا تأتي فترات الحياة (Lifetimes) — تعليقات تُخبر المُجمِّع بالعلاقة بين أعمار المراجع:
// بدون فترة حياة صريحة — المُجمِّع يستنتجها
fn first_element(list: &[f64]) -> &f64 {
&list[0]
}
// مع فترة حياة صريحة — عندما هناك عدة مراجع مُدخلة
fn longer_name<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
'a يقول للمُجمِّع: "المرجع المُخرج يعيش على الأقل بقدر أقصر المدخلين عمراً". في معظم الحالات، المُجمِّع يستنتج فترات الحياة تلقائياً ولا تحتاج كتابتها.
مثال عملي: إعدادات مستشعر مشتركة
struct SensorConfig {
name: String,
unit: String,
min_threshold: f64,
max_threshold: f64,
}
fn check_reading(config: &SensorConfig, value: f64) -> &str {
if value < config.min_threshold {
"أقل من الحد الأدنى"
} else if value > config.max_threshold {
"أعلى من الحد الأقصى"
} else {
"ضمن النطاق الطبيعي"
}
}
fn log_reading(config: &SensorConfig, value: f64) {
println!("[{}] {:.1} {} — {}",
config.name, value, config.unit, check_reading(config, value));
}
fn main() {
let temp_config = SensorConfig {
name: String::from("مستشعر_حرارة_01"),
unit: String::from("°C"),
min_threshold: 10.0,
max_threshold: 80.0,
};
// نفس الإعدادات تُشارك (بالاستعارة) مع عدة دوال
log_reading(&temp_config, 23.5);
log_reading(&temp_config, 85.0);
log_reading(&temp_config, 5.0);
// temp_config لا تزال ملكنا — يمكن استخدامها لاحقاً
println!("اسم المستشعر: {}", temp_config.name);
}
هنا SensorConfig تُستعار بواسطة عدة دوال — كل منها تقرأ الإعدادات بدون نسخها أو نقل ملكيتها.
الخلاصة
الاستعارة تسمح بمشاركة البيانات بأمان: &T للقراءة (عدة مراجع مسموحة) و &mut T للكتابة (مرجع واحد حصري). المُجمِّع يمنع سباقات البيانات والمراجع المعلّقة وقت التجميع. فترات الحياة تضمن أن المراجع لا تعيش أطول من بياناتها. هذا النظام يعني: أمان مطلق بدون كلفة أداء. في الدرس القادم سنتعلم كيف نُنشئ أنواع بيانات مخصصة باستخدام الهياكل والتعدادات.