لماذا &String و &Vec و &Box تعتبر ممارسة سيئة في Rust؟

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: الفخ الذي يقع فيه جميع المبتدئين في Rust

\n\n

أنت تتعلم Rust. لقد فهمت بالفعل مفهوم الملكية والاقتراض. أنت تكتب أول دالة جادة لك وتفكر: "حسنًا، لكي لا أنقل السلسلة النصية، سأمرر مرجعًا!". وهكذا تكتب:

\n\n
fn print_string(s: &String) {\n    println!("{}", s);\n}
\n\n

المترجم سعيد. الكود يعمل. أنت سعيد. لكن المشكلة: مطور Rust متمرس، عندما يرى كودك، سيتجهم. لماذا؟ أليس كل شيء يعمل؟

\n\n

هذا السؤال هو واحد من أكثر الأسئلة شيوعًا على StackOverflow. آلاف المطورين يتعثرون في هذه التفاصيل التي تبدو بسيطة. اليوم سنفهم لماذا &String و &Vec و &Box في توقيع الدالة تعتبر علامة حمراء، وكيفية الكتابة بشكل صحيح.

\n\n

المشكلة رقم 1: تقييد الأنواع الضمني (Type Coercion)

\n\n

Rust لديها نظام قوي للتحويل التلقائي للأنواع، ولكن فقط في اتجاه واحد. هذا يُسمى Deref Coercion (التحويل عبر إلغاء المرجع).

\n\n

الفكرة بسيطة: إذا كان لديك مرجع إلى String، يمكن لـ Rust تلقائيًا تحويله إلى مرجع إلى &str. نفس الشيء مع &Vec<T> → &[T] و &Box<T> → &T.

\n\n

انظر إلى الفرق:

\n\n
// سيء: يقبل فقط &String\nfn bad(s: &String) {\n    println!("{}", s);\n}\n\n// جيد: يقبل &str\nfn good(s: &str) {\n    println!("{}", s);\n}\n\nfn main() {\n    let owned = String::from("مرحبًا بالعالم!");\n    let slice = "مرحبًا، شريحة!";\n    \n    bad(&owned); // يعمل\n    // bad(slice); // خطأ! slice هو &str، وليس &String\n    \n    good(&owned); // يعمل (تحويل تلقائي)\n    good(slice);   // يعمل (هذا بالفعل &str)\n}
\n\n

أرأيت؟ الدالة good تقبل أي سلسلة نصية: سواء كانت مملوكة String أو حرفية &str أو حتى جزءًا من سلسلة. الدالة bad تقبل فقط String. هذا تقييد غير ضروري.

\n\n

مثال عملي: البحث عن نص فرعي

\n\n
// بحث محدود\nfn find_in_string(haystack: &String, needle: &str) -> bool {\n    haystack.contains(needle)\n}\n\n// بحث شامل\nfn find_in_str(haystack: &str, needle: &str) -> bool {\n    haystack.contains(needle)\n}\n\nfn main() {\n    let my_string = String::from("Rust رائع");\n    let my_str = "Rust رائع";\n    \n    // يعمل فقط مع String\n    find_in_string(&my_string, "Rust");\n    // find_in_str(my_str, "Rust"); // خطأ في الترجمة!\n    \n    // يعمل مع كليهما\n    find_in_str(&my_string, "Rust");\n    find_in_str(my_str, "Rust");\n}
\n\n

المشكلة رقم 2: الأداء والمراجع غير المباشرة الزائدة

\n\n

دعنا ننظر "تحت الغطاء". ما هو String في الذاكرة؟ هو هيكل من ثلاثة حقول:

\n
    \n
  • مؤشر إلى البيانات في الكومة (heap)
  • \n
  • طول السلسلة النصية
  • \n
  • السعة (capacity)
  • \n
\n\n

عندما تمرر &String، فأنت تمرر مرجعًا إلى هذا الهيكل. للوصول إلى البيانات، يحتاج المترجم إلى:

\n
    \n
  1. إلغاء مرجع &String → الحصول على هيكل String
  2. \n
  3. أخذ المؤشر إلى البيانات في الكومة من الهيكل
  4. \n
  5. إلغاء مرجع هذا المؤشر → الحصول على بايتات السلسلة
  6. \n
\n\n

عندما تمرر &str، فأنت تمرر مؤشرًا سمينًا (fat pointer) يتكون من حقلين فقط:

\n
    \n
  • مؤشر إلى البيانات
  • \n
  • الطول
  • \n
\n\n

يحتاج المترجم إلى خطوة واحدة فقط — إلغاء مرجع المؤشر. مراجع غير مباشرة أقل يعني كود أسرع. الفرق ضئيل جدًا، لكن في الحلقات الساخنة أو عند التمرير إلى

المدونات

توصيات الكتب