ما الذي يخفيه "المؤشر السمين"؟ تحليل المؤشر السمين (fat pointer) في Rust
\n\nتخيل أنك ذهبت إلى مكتبة وطلبت كتابًا. يعطيك أمين المكتبة ورقة صغيرة تحتوي على رقم الرف والرف — هذا هو المؤشر العادي. ولكن ماذا لو كنت لا تعرف حجم الكتاب؟ أو ربما هو كتاب من ثلاثة أجزاء؟ ستحتاج إلى ورقة ثانية: "الجزء 1 من 3" أو "الطول: 450 صفحة".
\n\nفي Rust، يُسمى هذا المؤشر "المزدوج" fat pointer (المؤشر السمين). إنه ليس سحرًا، بل مجرد زوج من الأرقام: عنوان البيانات ومعلوماتها الوصفية (غالبًا الطول أو مؤشر إلى جدول الطرق الافتراضية). دعنا نفهم لماذا نحتاج هذا وكيف يعمل، من خلال أمثلة كود حقيقية.
\n\nالمؤشرات العادية: رفيعة وسريعة
\n\nلنبدأ بالأساسيات. في Rust، هناك مؤشرات بسيطة — &T (مرجع) أو *const T (مؤشر خام). تشغل هذه المؤشرات 8 بايت بالضبط (على نظام 64 بت) وتخزن فقط العنوان في الذاكرة.
fn main() {\n let x: i32 = 42;\n let ptr: &i32 = &x; // مؤشر رفيع — 8 بايت\n println!("حجم المرجع: {} بايت", std::mem::size_of_val(&ptr));\n}\n\n\nالنتيجة: حجم المرجع: 8 بايت. كل شيء متوقع: المترجم يعرف أن i32 هو 4 بايت، لذلك لا حاجة لمعلومات إضافية. ولكن ماذا لو كان حجم البيانات غير معروف مسبقًا؟
أول مؤشر سمين: الشرائح (&[T])
\n\nالشريحة (slice) هي "نافذة" على مصفوفة أو متجه (vector). ولكن لاستخدامها، تحتاج إلى معرفة ليس فقط البداية، ولكن أيضًا عدد العناصر. يخزن Rust هذا كزوج (ptr, len) — وهذا هو المؤشر السمين.
fn main() {\n let arr = [1, 2, 3, 4, 5];\n let slice: &[i32] = &arr[1..4]; // شريحة من 3 عناصر\n\n println!("حجم الشريحة: {} بايت", std::mem::size_of_val(&slice));\n println!("العناصر: {:?}", slice);\n}\n\n\nالنتيجة: حجم الشريحة: 16 بايت (8 بايت للمؤشر + 8 بايت للطول). إذا كنا نمرر العنوان فقط، فلن نعرف أين نتوقف. لهذا السبب تعتبر الشريحة مثالًا كلاسيكيًا على المؤشر السمين.
بالمناسبة، السلسلة النصية &str تعمل بنفس الطريقة تمامًا: إنها مؤشر سمين (*const u8, usize). ولهذا السبب تشغل &str 16 بايت، بينما تشغل &String 8 بايت فقط (لأن String يخزن الطول داخليًا).
ثاني مؤشر سمين: كائنات السمات (&dyn Trait)
\n\nفي Rust، هناك إرسال ديناميكي (dynamic dispatch) عبر كائنات السمات (trait objects). عندما تكتب &dyn MyTrait، لا يعرف المترجم أي نوع محدد سيتم تمريره. يحتاج إلى جدول الطرق الافتراضية (vtable) لاستدعاء الدوال الصحيحة.
trait Animal {\n fn speak(&self);\n}\n\nstruct Dog;\nstruct Cat;\n\nimpl Animal for Dog {\n fn speak(&self) { println!("هاو!"); }\n}\n\nimpl Animal for Cat {\n fn speak(&self) { println!("مياو!"); }\n}\n\nfn main() {\n let dog = Dog;\n let cat = Cat;\n\n let animals: [&dyn Animal; 2] = [&dog, &cat];\n\n println!("حجم &dyn Animal: {} بايت", std::mem::size_of::<&dyn Animal>());\n\n for animal in &animals {\n animal.speak();\n }\n}\n\n\nالنتيجة: حجم &dyn Animal: 16 بايت. هذه الـ 16 بايت هي مرة أخرى مؤشر سمين: أول 8 بايت هي عنوان البيانات (Dog أو Cat)، والـ 8 بايت الثانية هي عنوان vtable (جدول الطرق).
كيف يؤثر المؤشر السمين على الأداء؟
\n\nالمؤشرات السمينة هي حل وسط. من ناحية، تحمل معلومات أكثر، مما يسمح بالعمل مع الأنواع الديناميكية. من ناحية أخرى:
\n\n- \n
- الذاكرة: المؤشر السمين يشغل 16 بايت بدلاً من 8. هذا ليس حرجًا، لكنه قد يؤثر في المجموعات الكبيرة. \n
- السرعة: الوصول عبر vtable (لكائنات السمات) أبطأ قليلاً من استدعاء الطريقة المباشر (التجسيد الأحادي - monomorphization). الفرق \n