لماذا لا تسمح لك Rust ببساطة بكتابة s[0]؟
\n\nهل بدأت للتو في تعلم Rust، وتكتب أول "Hello, world!" لك، وفجأة... خطأ في الترجمة. تحاول الحصول على الحرف الأول من السلسلة النصية، كما فعلت مئات المرات في Python أو JavaScript، ولكن المترجم يرمي شيئًا مثل the type 'String' cannot be indexed by '{integer}'. هل هذا مألوف؟
هذا السؤال هو واحد من أكثر الأسئلة شيوعًا على Stack Overflow. آلاف المطورين يتعثرون في هذه "الغرابة" الظاهرية لـ Rust. ولكن في الحقيقة، وراء هذا يكمن حل عبقري ومدروس جيدًا، يحمي كودك من أخطاء يصعب اكتشافها. في هذه المقالة، سنستعرض أهم الأخطاء عند فهرسة السلاسل النصية في Rust، والأهم من ذلك، سنتعلم كيفية القيام بذلك بشكل صحيح.
\n\nالخطأ رقم 1: محاولة الفهرسة المباشرة كما في C أو Python
\n\nالخطأ الأول والأكثر شيوعًا هو محاولة كتابة let c = my_string[0];. في Rust، السلاسل النصية ليست مجرد مصفوفات من أحرف ASCII. يتم تخزينها بترميز UTF-8. وهذا يغير كل شيء.
لماذا لا يعمل هذا؟
\nتخيل أن لديك سلسلة نصية "Привет". فيها 6 "أحرف" (من وجهة نظر الإنسان). لكن في ذاكرة الكمبيوتر، تشغل 12 بايت (كل حرف سيريليكي يشغل 2 بايت). إذا سمحت Rust بـ my_string[0]، ماذا كانت سترجع لك؟ أول بايت (الذي لا معنى له في حد ذاته) أم الحرف الأول؟ وماذا لو كانت السلسلة النصية تحتوي على رمز تعبيري يشغل 4 بايت؟ لكانت النتيجة فوضى عارمة.
تضمن Rust الأمان على مستوى الترجمة. الفهرسة برقم صحيح دائمًا تعمل بتعقيد O(1) وتعيد عنصرًا واحدًا. لكن بالنسبة لـ UTF-8، هذا مستحيل: "الحرف" الواحد (char) يمكن أن يشغل من 1 إلى 4 بايت. لذلك، يمنع المترجم ببساطة هذه العملية، حتى لا تحصل بالصدفة على "بيانات غير مفيدة" أو تفسد الترميز.
\n\n// ❌ خطأ في الترجمة!\nfn main() {\n let s = String::from("Привет");\n // let first = s[0]; // error[E0277]: the type 'String' cannot be indexed by '{integer}'\n println!("Привет, мир!");\n}\n\nالخطأ رقم 2: الاستخدام الساذج للشرائح (slicing) دون مراعاة حدود الأحرف
\n\nنعم، الشرائح في Rust تعمل. يمكنك كتابة &s[0..5]. لكن إذا وقعت "داخل" حرف متعدد البايتات، فسينهار برنامجك مع حالة ذعر (panic) أثناء التنفيذ. وهذا يسمى runtime panic.
مثال على خطأ قاتل
\nfn main() {\n let s = String::from("Здравствуйте"); // 12 حرفًا، لكن 24 بايت\n // لنحاول أخذ أول 3 بايتات\n let slice = &s[0..3]; // 'З' يشغل 2 بايت، 'д' يشغل 2 بايت.\n // 3 بايتات هي نصف الحرف الثاني!\n println!("{}", slice); // thread 'main' panicked at 'byte index 3 is not a char boundary'\n}\n\nهذا ليس مجرد تحذير. إنه انهيار كامل للبرنامج. Rust لا تغفر العمل غير الآمن مع الذاكرة، حتى في وقت التشغيل. إذا كنت تتعامل مع إدخال المستخدم (أسماء مستعارة، تعليقات)، فستحصل بالتأكيد على هذا الذعر (panic) عند أول حرف سيريليكي أو آسيوي.
\n\nكيف تتجنب ذلك: استخدم الطريقة char_indices() للبحث الآمن عن الحدود، أو اعمل من خلال chars().
الخطأ رقم 3: الخلط بين البايتات، char، والغرافيمات
\n\nهذا هو أخطر خطأ. حتى إذا تعلمت كيفية الحصول على char من السلسلة النصية، فقد لا تزال تحصل على شيء غير متوقع. لأن char في Rust هو قيمة عددية لـ Unicode (Unicode Scalar Value). وما يعتبره الإنسان "حرفًا" (مثل "é" أو "🇷🇺")، قد يتكون من عدة char.
تحليل بمثال
\nfn main() {\n let s = String::from("é"); // يمكن أن يكون هذا char واحد (U+00E9)\n // أو مركبًا: 'e' (U+0065) + '◌́' (U+0301)\n let composed = String::from("e\\u{0301}"); // نفس 'é'، لكن من اثنين char\n println!("طول s: {} بايت، {} char", s.len(), s.chars().count());\n println!("طول comp