اذهب إلى المحتوى

المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست


Naser Dakhel

كانت مشكلتنا باستخدام الصف tuple في المقالة السابقة (الشيفرة 4-5) هو أنه علينا إعادة النوع String إلى القيمة المُستدعية ليتسنى لنا استخدام النوع String حتى بعد استدعاء الدالة calculate_length، وذلك لأن النوع String نُقل إلى calculate_length. بدلًا مما سبق يمكننا استخدام مرجع reference إلى القيمة String؛ والمرجع هو أشبه بالمؤشر pointer، إذ يُمثل عنوانًا يمكنك اتباعه للوصول إلى البيانات المخزنة ضمن العنوان المذكور، وتعود ملكية هذه البيانات إلى متغيرات أخرى مختلفة. من المضمون للمراجع -على عكس المؤشرات- أن تُشير إلى قيمة صالحة لنوع معين طوال دورة حياة المرجع.

إليك كيفية تعريف الدالة calculate_length واستخدامها مع احتوائها على مرجع لكائن بمثابة معاملٍ لها بدلًا من أخذ ملكية القيمة:

اسم الملف: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

لاحظ أولًا أننا أزلنا الشيفرة البرمجية الخاصة بالصف في تصريح المتغير وقيمة الدالة المُعادة، كما أننا مررنا أيضًا s1& إلى الدالة calculate_length وأخذنا في تعريفها String& بدلًا من String وتُمثّل هذه الإشارة & المرجع، وتسمح لك بالإشارة إلى قيمة دون أخذ ملكيتها، ويوضح الشكل التالي هذا المفهوم.

مخطط يوضح String s& الذي يُشير إلى String s1

[شكل 5: مخطط يوضح String s& الذي يُشير إلى String s1]

ملاحظة: عكس عملية المرجع باستخدام & هي التحصيل dereferencing ويمكن تحقيقها باستخدام عامل التحصيل * وسنرى بعض استخدامات التحصيل بالإضافة لتفاصيل العملية لاحقًا.

دعنا نأخذ نظرةً أعمق على استدعاء الدالة هنا:

    let s1 = String::from("hello");

    let len = calculate_length(&s1);

تسمح لك الكتابة s1& بإنشاء مرجع يشير إلى القيمة s1 إلا أنه لا ينقل الملكية، وبالتالي -وبما أنه لا يملكها- لن تُسقط القيمة (باستخدام drop) التي يشير إليها عندما يتوقف المرجع عن استخدامها.

وبالمثل، تستخدم شارة الدالة الرمز & للإشارة إلى أن نوع المعامل s هو مرجع. دعنا نضيف بعض التعليقات التوضيحية:

fn calculate_length(s: &String) -> usize { // ‫يمثل s مرجعًا إلى String
    s.len()
} // ‫يخرج s من النطاق هنا إلا أنه لا يُسقط لأنه لا يمتلك القيمة التي يشير إليها

النطاق الذي يحتوي على المتغير s هو مماثل لأي نطاق معامل دالة، إلا أن القيمة المُشار إليها باستخدام المرجع لا تُسقَط عندما نتوقف عن استخدام s وذلك لأن s لا يملك القيمة. لا نحتاج لإعادة القيم عندما تحتوي الدوال على مراجع مثل معاملات بدلًا من القيم الفعلية حتى نستطيع منح الملكية مجددًا، وذلك لأننا لم ننقل الملكية في المقام الأول.

نطلق على عملية إنشاء المراجع عملية الاستعارة borrowing، فكما الحال في الحياة الواقعية، إذا امتلك شخصٌ ما غرضًا، فيمكنك استعارته منه، وعند الانتهاء من الغرض عليك أن تُعيده إلى ذلك الشخص لأنك لا تملك الغرض.

ما الذي يحصل إذًا عندما نحاول التعديل على شيء مُستعار؟ جرّب تنفيذ الشيفرة 4-6 (تحذير: لن تعمل)

اسم الملف: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

[الشيفرة 4-6: محاولة التعديل على قيمة مُستعارة]

وسيظهر الخطأ التالي:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

المراجع غير قابلة للتعديل immutable افتراضيًا كما هو الحال مع المتغيرات، لذا لا يُمكنك التعديل على شيء باستخدام المرجع.

المراجع القابلة للتعديل

يُمكننا تصحيح الشيفرة 4-6 السابقة لكي تسمح لنا بتعديل قيمة مُستعارة باستخدام تعديلات بسيطة، وذلك باستخدامنا مرجعًا قابلًا للتعديل mutable reference:

اسم الملف: src/main.rs

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

علينا أولًا أن نعدل المتغير s ليصبح mut، ثم نُنشئ مرجعًا قابلًا للتعديل باستخدام mut s& عند نقطة استدعاء الدالة chang، ومن ثم تحديث شارة الدالة حتى تقبل مرجعًا قابلًا للتعديل بكتابة some_string: &mut String، إذ يدلنا هذا بكل وضوح على أن الدالة chang ستُعدّل من القيمة التي استعارتها.

للمراجع القابلة للتعديل قيدٌ واحدٌ كبير، وهو أنه لا يمكنك الحصول على أكثر من مرجع إلى قيمة إذا كان لتلك القيمة مرجعًا قابلًا للتعديل. نُحاول في الشيفرة البرمجية التالية إنشاء مرجعين قابلَين للتعديل يشيران إلى s:

اسم الملف: src/main.rs

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

إليك الخطأ:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

يدل هذا الخطأ على أن الشيفرة البرمجية غير صالحة لأنه لا يُمكننا استعارة القيمة s على أنها قيمة قابلة للتعديل أكثر من مرة واحدة، إذ نستعير القيمة القابلة للتعديل للمرة الأولى في r1 ويجب أن نحافظ على الاستعارة حتى تُستخدم القيمة في !println، إلا أننا حاولنا أيضًا إنشاء مرجع قابل للتعديل آخر في r2 يستعير نفس البيانات الموجودة في r1 وذلك بين إنشاء المرجع القابل للتعديل الأول وبين استخدامه.

يسمح القيد الذي يمنع وجود عدة مراجع قابلة للتعديل تشير لنفس البيانات في نفس الوقت بتعديل البيانات ولكن بطريقة مُقيّدة جدًا، وهو شيء يعاني منه معظم متعلمي لغة رست الجدُد وذلك لأن معظم اللغات تسمح لك بتعديل ما تشاء. الميزة من هذا القيد هو أن رست تمنع سباق البيانات data races عند وقت التصريف، وسباق البيانات هو مشابه لحالة السباق race condition ويحدث عند حدوث أحد الحالات الثلاث:

  • محاولة مؤشرين أو أكثر الوصول إلى نفس البيانات في نفس الوقت.
  • استخدام واحد من المؤشرات على الأقل للكتابة إلى البيانات.
  • عدم وجود آلية مُستخدمة لمزامنة الوصول إلى البيانات.

تتسبب سباقات البيانات بسلوك غير معرّف undefined behaviour وقد يكون تشخيص المشكلة وتصحيحها صعبًا عندما تحاول تتبع الخطأ عند وقت التشغيل، وتمنع رست هذه المشكلة برفض تصريف الشيفرة البرمجية التي تحتوي على سباقات البيانات.

يُمكننا استخدام الأقواس المعقوصة curly brackets لإنشاء نطاق جديد، مما يسمح بوجود مراجع قابلة للتعديل، إلا أن الشرط هو عدم تواجد المراجع القابلة للتعديل ضمن النطاق في ذات الوقت:

    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // يخرج‫ r1 من النطاق هنا وبالتالي يمكننا إنشاء مرجع جديد دون أي مشاكل

    let r2 = &mut s;

تُجبرنا رست على قاعدة مشابهة لجمع المراجع القابلة وغير القابلة للتعديل. تولّد الشيفرة البرمجية التالية خطأً:

    let mut s = String::from("hello");

    let r1 = &s; // لا توجد مشكلة
    let r2 = &s; // لا توجد مشكلة
    let r3 = &mut s; // هناك مشكلة كبيرة

    println!("{}, {}, and {}", r1, r2, r3);

إليك الخطأ:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

كما أنه لا يمكننا إنشاء مرجع قابل للتعديل بينما لدينا مرجع غير قابل للتعديل يشير إلى القيمة ذاتها.

لا يتوقع مستخدموا المراجع غير القابلة للتعديل بأن تتغير القيمة من تلقاء نفسها، لكن تسمح المراجع المتعددة غير القابلة للتعديل بذلك لأنه لا يوجد أي شيء يقرأ البيانات ولديه القدرة على التأثير على أي من المراجع التي تقرأ البيانات.

لاحظ بأن نطاق المراجع يبدأ من مكان إنشائها ويستمر إلى آخر مكان استُخدم فيه المرجع، فعلى سبيل المثال، يمكن تصريف الشيفرة البرمجية التالية بنجاح لأن استخدام المراجع غير القابلة للتعديل الأخير في !println يحدث قبل إنشاء المرجع القابل للتعديل:

    let mut s = String::from("hello");

    let r1 = &s; // لا يوجد مشكلة
    let r2 = &s; // لا يوجد مشكلة
    println!("{} and {}", r1, r2);
    // لن يُستخدم المتغيرين‫ r1 و r2 بعد هذه النقطة

    let r3 = &mut s; // لا يوجد مشكلة
    println!("{}", r3);

ينتهي نطاق كل من المرجعَين غير القابلَين للتعديل r1 و r2 بعد !println وهي آخر نقطة لاستخدامهما قبل إنشاء المرجع القابل للتعديل r3. لا تتداخل النطاقات ولذلك تكون الشيفرة البرمجية صالحة، وتدعى قدرة المصرف على معرفة المراجع التي لا تُستخدم بعد الآن في نهاية النطاق باسم دورات الحياة غير المُعجمية Non-Lexical Lifetimes -أو اختصارًا NLL -ويمكنك القراءة عنها من rust-lang.org.

على الرغم من كون أخطاء الاستعارة أخطاءً مثيرةً للإحباط في بعض الأحيان، إلا أنه يجب عليك أن تتذكر أن مصرّف رست يُشير إلى خطأ قبل أوانه (عند وقت التصريف بدلًا من وقت التشغيل) ويوضح لك بالتفصيل مكان المشكلة حتى لا تضطر إلى تعقب المشكلة إذا كانت بياناتك تحتوي قيمًا مختلفة لتوقعاتك.

المراجع المعلقة

من السهل في اللغات التي تدعم المؤشرات أن تُنشئ مؤشرات معلقة dangling pointer بصورةٍ خاطئة؛ وهي مؤشرات تشير إلى موضع في الذاكرة مُعطًى لشخص آخر وذلك بتحرير المساحة مع المحافظة على المؤشر الذي يشير إلى الذاكرة، وهذا غير ممكن الحصول في رست، فعلى النقيض تمامًا يضمن المصرف ألا تكون جميع المراجع مُعلّقة، لأنه سيتأكد من عدم مغادرة البيانات التي يشير إليها المرجع النطاق قبل أن يُغادر المرجع النطاق أولًا.

دعنا نجرّب إنشاء مرجع معلّق لملاحظة كيف تمنع رست وجودها بخطأ عند وقت التصريف:

اسم الملف: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

إليك الخطأ:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

يُشير هذا الخطأ إلى الميزة التي لم نكتشفها بعد، ألا وهي دورات الحياة lifetimes، وسنناقشها بتوسُّع لاحقًا، ولكن إذا تغاضينا عن الجزء الخاص بدورات الحياة، فإن رسالة الخطأ تحتوي سبب المشكلة:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

دعنا نأخذ نظرةً أقرب لما يحدث في كل مرحلة من مراحل الدالة dangle:

اسم الملف: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String { // تُعيد الدالة‫ dangle مرجعًا إلى النوع String

    let s = String::from("hello"); // ‫يشكل s السلسلة String الجديدة

    &s // نُعيد المرجع إلى النوع‫ String واسمه s
} // ‫يخرج s من النطاق هنا ويُسقَط وتُحرّر ذاكرته. خطر!

ستُحرّر s عند الانتهاء من تنفيذ الشيفرة البرمجية داخل الدالة dangle، لأن s مُنشأة بداخل dangle، إلا أننا حاولنا إعادة مرجع إليها وهذا يعني أن المرجع سيشير إلى قيمة String غير صالحة، وهذا ما يجب تجنُّب حدوثه، بالتالي لن تسمح لنا رست بفعل ذلك.

يكمن الحل هنا بإعادة String مباشرةً:

fn main() {
    let string = no_dangle();
}
fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

يعمل هذا الحل دون أي مشاكل، إذ تُنقل الملكية ولا يُحرّر أي حيز من الذاكرة.

قوانين المراجع

دعنا نلخص أهم النقاط التي ناقشناها بخصوص المراجع:

  • يُمكنك في نقطة من الزمن استخدام مرجع واحد قابل للتعديل أو عدة مراجع غير قابلة للتعديل.
  • يجب أن تكون المراجع صالحة على الدوام.

سننظر في الفقرات التالية إلى نوع مختلف من المراجع هو الشرائح slices.

نوع الشريحة

تسمح لك الشرائح بالإشارة reference إلى سلسلة متتابعة من العناصر ضمن تجميعة collection بدلًا من الإشارة إلى كامل التجميعة، وتشبه الشريحة المرجع ولذا فهو لا يحتوي على ملكية.

إليك مشكلةً برمجيةً صغيرة: اكتب دالةً تأخذ سلسلةً نصيةً من كلمات مفصول ما بينها بمسافات وتُعيد الكلمة الأولى التي تجدها ضمن تلك السلسلة النصية، إذا لم تعثر الدالة على مسافة ضمن السلسلة النصية، فهذا يعني أن السلسلة النصية مؤلفةٌ من كلمة واحدة فقط وعليها أن تُعيد كامل السلسلة النصية.

لنرى كيف يمكننا كتابة بصمة signature الدالة دون استخدام الشرائح، وذلك حتى نفهم المشكلة التي تحلها الشرائح:

fn first_word(s: &String) -> ?

للدالة first_word معامل ‎&String، وهذا ما لا بأس فيه لأننا لا نحتاج الملكية عليه، لكن ما الذي يجب أن تُعيده الدالة؟ لا توجد لدينا أي طريقة لتحديد جزء من السلسلة النصية، إلا أننا نستطيع إعادة دليل index نهاية الكلمة بالاستفادة من المسافة، لنجرّب هذه الطريقة كما هو موضح في الشيفرة 4-7.

اسم الملف: src/main.rs

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

[الشيفرة 4-7: الدالة first_word تُعيد قيمة دليل بحجم بايت إلى معامل String]

علينا أن نحوّل String إلى مصفوفة من البايتات لأننا بحاجة للمرور بعناصر String عنصرًا تلو الآخر للبحث عن مسافات، وذلك باستخدام التابع as_bytes:

let bytes = s.as_bytes();

من ثم نُنشئ مُكرّرًا iterator على مصفوفة البايتات باستخدام تابع iter:

for (i, &item) in bytes.iter().enumerate() {

سنُناقش المُكرّرات بالتفصيل لاحقًا، أما الآن فكل ما عليك معرفته هو أن iter تابع يُعيد كل عنصر في تجميعة، وأن enumerate يُغلّف wraps نتيجة iter ويُعيد كل عنصر على أنه جزءٌ من الصف tuple بدلًا من ذلك، إذ يمثّل العنصر الأول من الصف المُعاد من التابع enumerate دليلًا، بينما يمثل العنصر الثاني مرجعًا إلى العنصر في التجميعة، وهذه الطريقة أسهل من حساب أدلة العناصر بأنفسنا.

يُمكننا استخدام الأنماط patterns لتفكيك الصف وذلك بالنظر إلى أن التابع enumerate يُعيد صفًا، وسنناقش الأنماط بالتفصيل لاحقًا. نُحدد في حلقة for النمط الذي يحتوي على i، والذي يمثل الدليل الموجود في الصف و ‎&item للبايت الوحيد الموجود في الصف، ونستخدم & في النمط لأننا نحصل على مرجع للعنصر من iter().enumerate()‎..

نبحث عن البايت الذي يمثل المسافة داخل حلقة for باستخدام البايت المجرّد، وإن وجدنا مسافةً نُعيد مكانها، وإلا فنعيد طول السلسلة النصية باستخدامs.len()‎:

        if item == b' ' {
            return i;
        }
    }

    s.len()

لدينا طريقةٌ الآن لمعرفة دليل نهاية الكلمة الأولى في السلسلة النصية، إلا أن هناك مشكلةُ في هذه الطريقة؛ إذ أننا نُعيد النوع usize بصورةٍ منفردة إلا أنه ذو معنى فقط في سياق ‎&String، بكلمات أخرى، لا يوجد أي ضمان أن القيمة ستكون صالحة في المستقبل نظرًا لأنها قيمة منفصلة عن String. ألقِ نظرةً على الشيفرة 4-8 التي تستخدم الدالة first_word من الشيفرة 4-7.

اسم الملف: src/main.rs

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // ‫ستُخزّن القيمة 5 في word

    s.clear(); // تُفرغ هذه التعليمة السلسلة النصية وتجعلها مساوية إلى القيمة‫ “”

    // ‫للمتغير word القيمة 5 هنا إلا أنه لا يوجد أي سلسلة نصية تجعل من هذه القيمة ذات معنى، وبالتالي القيمة عديمة الفائدة!
}

[الشيفرة 4-8: تخزين القيمة من استدعاء الدالة first_word ومن ثمّ تغيير محتويات String]

يُصرَّف البرنامج السابق دون أي أخطاء وسيعمل دون مشاكل إذا استخدمنا word بعد استدعاء s.clear()‎، وذلك لأن word ليست مرتبطة بحالة sإطلاقًا، إذ ما زالت تحتوي word على القيمة 5 حتى بعد استدعاء s.clear()‎، ويمكننا استخدام القيمة 5 مع المتغير s حتى نحاول استخراج الكلمة الأولى إلا أن ذلك سيتسبب بخطأ لأن محتويات s تغيرت منذ أن خزّننا 5 في word.

تُعد مراقبة الدليل الموجود في word خشيةً من فقدان صلاحيته بالنسبة للمتغير s عمليةً رتيبةً ومعرضةً للخطأ، وستصبح أسوأ إذا كتبنا دالة second_word تكون بصمتها على النحو التالي:

fn second_word(s: &String) -> (usize, usize) {

الآن وبما أننا نتتبع دليل البداية والنهاية، فهذا يعني أنه لدينا قيم أكثر لحسابها من البيانات في حالة معينة، إلا أن هذه القيم غير مرتبطة بحالة ما إطلاقًا، أصبح لدينا الآن إذًا ثلاثة متغيرات غير مرتبطة مع بعضها بعضًا ويجب أن نربطها.

لدى رست لحسن الحظ الحل لهذه المشكلة، وهي سلسلة الشرائح النصية string slices.

شرائح السلاسل النصية

شرائح السلاسل النصية هي مرجعٌ لجزء من النوع String وتُكتب بالشكل التالي:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

نستخدم المرجع hello الذي يمثّل مرجعًا لجزء من النوع String بدلًا من استخدام مرجع لكامل النوع، والجزء مُحدّد باستخدام [5..0] بت. نُنشئ هنا شرائحًا باستخدام مجال باستخدام الأقواس وذلك بتحديد دليل البداية ودليل النهاية بالشكل التالي: [starting_index..ending_index]؛ إذ يُمثل starting_index موضع بداية الشريحة؛ بينما يمثل ending_index الموضع الذي يلي موضع نهاية الشريحة. يُخزّن هيكل بيانات الشريحة داخليًا كلًا من موضع البداية وطول الشريحة، الذي تحصل عليه من طرح ending_index من starting_index، لذا في حالة let world = &s[6..11]‎ نحصل على شريحة باسم world تحتوي على مؤشر يشير إلى البايت الموجود في الدليل 6 للسلسلة s بقيمة طول مساوية إلى 5.

يوضح الشكل 6 العملية السابقة.

شريحة سلسلة نصية تُشير إلى جز

[شكل 6: شريحة سلسلة نصية تُشير إلى جزء من String]

يُمكنك إهمال دليل البدء قبل النقطتين .. في المجال إذا أردت البدء من الدليل صفر، أي أن الشريحتين التاليتن متماثلتان:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

كما يمكنك إهمال دليل النهاية إذا أردت أن تُضمّن السلسلة النصية إلى النهاية، أي أن الشريحتين التاليتين متماثلتان:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

أخيرًا، يمكنك إهمال كل من دليل البداية ودليل النهاية ضمن المجال إذا أردت الحصول على كامل السلسلة النصية، والشريحتان التاليتان متماثلتان:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

ملاحظة: يجب أن تمثّل أدلّة شرائح السلاسل النصية محرفًا صالحًا من ترميز UTF-8، وإلا سيتوقف البرنامج ويعرض خطأً إذا حاولت إنشاء شريحة سلسلة نصية منتصف محرف متعدد البايتات multibyte character، ونفرض هنا في هذا القسم أن المحارف بترميز آسكي ASCII فقط بهدف البساطة، وسنناقش مفصّلًا التعامل مع محارف بترميز UTF-8 لاحقًا.

بعد تعرّفنا لما سبق، دعنا نكتب دالة تُعيد شريحة نسميها first_word، والنوع الذي يمثّل شريحة السلسلة النصية هو str&:

اسم الملف: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
fn main() {}

نحصل في البرنامج السابق على دليل نهاية الكلمة بصورةٍ مشابهة لما فعلناه في الشيفرة 4-7 وذلك بالبحث عن أوّل ظهور لمسافة، وعندما نجد هذه المسافة نُعيد شريحة سلسلة نصية باستخدام بداية السلسلة النصية مثل دليل بداية ودليل المسافة مثل دليل نهاية.

نحصل على قيمة واحدة متعلقة بالبيانات التي لدينا بعد استدعاء الدالة first_word، وتتألف القيمة من مرجع إلى نقطة البداية لشريحة السلسلة النصية وعدد العناصر في تلك الشريحة.

يمكننا أن نجعل الدالة second_word تُعيد شريحة أيضًا:

fn second_word(s: &String) -> &str {

أصبح لدينا الآن واجهة برمجية API واضحة صعب العبث فيها، إذ سيتأكد المصرّف من أن مراجع النوع String هي مراجع صالحة. أتتذكر الخطأ الذي واجهناه في البرنامج الموجود في الشيفرة 4-8 عندما حصلنا على دليل نهاية الكلمة الأولى ومن ثمّ مسحنا السلسلة النصية مما جعل الدليل غير صالح؟ كانت الشيفرة تلك غير صحيحة منطقيًا ولكننا لم نحصل على أي أخطاء مباشرة واضحة، وستظهر المشكلة لاحقًا إذا حاولت استخدام دليل السلسلة النصية الفارغة، إلا أن شرائح السلاسل النصية تجعل من هذا الخطأ مستحيلًا وستعلمنا بحدوث خطأ في شيفرتنا البرمجية في وقت مبكّر، وعلى سبيل المثال، نحصل على خطأ وقت التصريف إذا استخدمنا إصدار شريحة السلسلة النصية من الدالة first_word.

اسم الملف: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // ‫خطأ!

    println!("the first word is: {}", word);
}

إليك خطأ التصريف:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

تذكّر من قواعد الاستعارة borrowing rules أنه لا يمكننا أخذ مرجع قابل للتعديل من شيء إذا كان لدينا مرجع غير قابل للتعديل لهذا الشيء مسبقًا، ولأن clear بحاجة لحذف محتويات String، فهي بحاجة للحصول على مرجع قابل للتعديل، يستخدم println!‎ بعد الاستدعاء للدالة clear المرجع في word، لذلك لا بدّ للمرجع غير القابل للتعديل أن يكون صالحًا بحلول تلك النقطة ولذلك تمنع رست وجود مرجع قابل للتعديل في clear ومرجع غير قابل للتعديل في word في الوقت ذاته مما يتسبب بفشل عملية التصريف. لم تكتفي رست بجعل الواجهة البرمجية أسهل للتعامل بل أقصَت صنفًا كاملًا من الأخطاء ممكنة الحدوث عند وقت التصريف.

السلاسل النصية المجردة هي شرائح

تذكر أننا تحدثنا عن السلاسل النصية المجردة بكونها تُخزّن بداخل الملف التنفيذي الثنائي، ويمكننا الآن فهم السلاسل النصية المجردة بوضوح بما أننا نعرف الشرائح:

let s = "Hello, world!";

نوع s هنا هو ‎&str وهي شريحة تُشير إلى جزء محدد من الملف الثنائي، وهذا السبب في كون السلاسل النصية غير قابلة للتعديل، وبالتالي يكون المرجع ‎&str مرجعًا غير قابل للتعديل.

شرائح السلاسل النصية مثل معاملات

تدلنا معرفة أنه يُمكننا أخذ شرائح من السلاسل النصية المجردة والقيم من النوع String على أنه نستطيع إجراء تحسين واحد إضافي على first_word وهو بصمة الدالة:

fn first_word(s: &String) -> &str {

قد يكتب مبرمج لغة رست خبير بصمة الدالة السابقة الموضحة في الشيفرة 4-9 بدلًا من ذلك والسبب في هذا هو أن البصمة السابقة تسمح لنا باستخدام الدالة ذاتها على قيمتَي ‎&String و ‎&str.

fn first_word(s: &str) -> &str {

[الشيفرة 4-9: تحسين دالة first_word باستخدام شريحة سلسلة نصية لنوع المُعامل s]

يُمكننا تمرير شريحة السلسلة النصية مباشرةً في هذه الحالة، فعلى سبيل المثال يمكننا تمرير شريحة من النوع String أو مرجع إليه إذا كان لدينا النوع String في معاملات الدالة، وهذا الأمر ممكن بفضل ميزة التحصيل القسري deref corecions التي سنتكلم عنها بالتفصيل لاحقًا. يجعل تعريف الدالة لتأخذ شريحة سلسلة نصية بدلًا من مرجع إلى النوع String من واجهتنا البرمجية شاملة الاستخدام أكثر ومفيدةً دون خسارة أي من وظائفها.

اسم الملف: src/main.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
fn main() {
    let my_string = String::from("hello world");

    // ‫تعمل الدالة first_word على شرائح النوع String سواءً كانت شريحة جزئية أو شريحة تشكل كامل السلسلة
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // ‫تعمل الدالة first_word على مراجع النوع String والمساوية إلى كامل شرائح String
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // ‫تعمل الدالة first_word على شرائح سلاسل نصية مجردة سواءً كانت مجردة أو جزئية
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // بما أن السلاسل النصية المجردة هي شرائح سلاسل نصية بالأصل، فالتعليمة التالية تعمل أيضًا دون طريقة كتابة الشريحة
    let word = first_word(my_string_literal);
}

الشرائح الأخرى

شرائح السلاسل النصية هي شرائح خاصة بالسلاسل النصية كما قد تتوقع، إلا أن هناك أنواع شرائح عامة أكثر، ألقِ نظرةً على المصفوفة التالية:

let a = [1, 2, 3, 4, 5];

قد نحتاج لاستخدام مرجع يشير إلى جزء من مصفوفة بطريقة مماثلة للسلسلة النصية، ويمكننا تحقيق ذلك الأمر وفق ما يلي:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

للشريحة النوع ‎&[i32]‎ وتعمل بطريقة مماثلة لشريحة السلسلة النصية وذلك بتخزين مرجع للعنصر الأول وطول الشريحة، وستستخدم هذا النوع من الشرائح لكافة أنواع التجميعات الأخرى، وسنناقش هذه التجميعات بالتفصيل عندما نتحدث عن الأشعة لاحقًا.

ترجمة -وبتصرف- لقسم من فصل Understanding Ownership من كتاب The Rust Programming Language

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...